Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changes/unreleased/Fixed-20260812-123144.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Fixed
body: Enable spock_output for Postgres 16.15+, 17.11+, and 18.5+ to restore replication slot creation.
time: 2026-08-12T12:31:44.954828+05:30
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (

"github.com/alessio/shellescape"
"github.com/pgEdge/control-plane/server/internal/database"
"github.com/pgEdge/control-plane/server/internal/ds"
"github.com/pgEdge/control-plane/server/internal/patroni"
"github.com/pgEdge/control-plane/server/internal/postgres"
"github.com/pgEdge/control-plane/server/internal/postgres/hba"
Expand Down Expand Up @@ -59,6 +60,9 @@ type PatroniConfigGenerator struct {
PatroniAllowlist []string `json:"patroni_allowlist"`
// PatroniPort is the port that Patroni will listen on.
PatroniPort int `json:"patroni_port"`
// PgEdgeVersion is the Postgres/Spock version for this instance. This is
// used to gate version-specific default Postgres parameters.
PgEdgeVersion *ds.PgEdgeVersion `json:"pg_edge_version,omitempty"`
// PgHbaConf are user-supplied pg_hba.conf entries (one rule per element),
// inserted in the user zone after the CP rules and before the catch-all.
PgHbaConf []string `json:"pg_hba_conf,omitempty"`
Expand Down Expand Up @@ -144,6 +148,7 @@ func NewPatroniConfigGenerator(opts PatroniConfigGeneratorOptions) *PatroniConfi
NodeSize: opts.Instance.NodeSize,
OrchestratorParameters: opts.OrchestratorParameters,
PatroniPort: opts.PatroniPort,
PgEdgeVersion: opts.Instance.PgEdgeVersion,
PostgresCertsDir: opts.Paths.Instance.PostgresCertificates(),
PostgresPort: opts.PostgresPort,
RestoreCommand: restoreCommand,
Expand Down Expand Up @@ -207,7 +212,7 @@ func (p *PatroniConfigGenerator) AuthMethod() hba.AuthMethod {
}

func (p *PatroniConfigGenerator) parameters() map[string]any {
parameters := postgres.DefaultGUCs()
parameters := postgres.DefaultGUCs(p.PgEdgeVersion)
maps.Copy(parameters, postgres.SpockDefaultGUCs())
maps.Copy(parameters, postgres.DefaultTunableGUCs(p.MemoryBytes, p.CPUs, p.ClusterSize))
maps.Copy(parameters, map[string]any{
Expand Down
41 changes: 39 additions & 2 deletions server/internal/postgres/gucs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,43 @@ package postgres

import (
"math"

"github.com/pgEdge/control-plane/server/internal/ds"
)

func DefaultGUCs() map[string]any {
return map[string]any{
// minOutputPluginLibrariesVersions are the earliest Postgres minor version per
// major version that gates output plugins behind the output_plugin_libraries
// allowlist GUC. Any major version newer than the last entry here is assumed
// to require it as well. See SPOC-651 / PLAT-721.
var minOutputPluginLibrariesVersions = map[uint64]*ds.Version{
16: ds.MustParseVersion("16.15"),
17: ds.MustParseVersion("17.11"),
18: ds.MustParseVersion("18.5"),
}

// needsOutputPluginLibraries reports whether the given Postgres version
// requires output_plugin_libraries to be set in order to allow spock_output
// to create replication slots.
func needsOutputPluginLibraries(version *ds.PgEdgeVersion) bool {
if version == nil || version.PostgresVersion == nil {
return false
}
pgVersion := version.PostgresVersion.MajorMinorVersion()
major, ok := pgVersion.Major()
if !ok {
return false
}
minVersion, ok := minOutputPluginLibrariesVersions[major]
if !ok {
// Newer majors than we know about are assumed to need it; older
// majors than we know about never had the gate.
return major > 18
}
return pgVersion.Compare(minVersion) >= 0
}

func DefaultGUCs(version *ds.PgEdgeVersion) map[string]any {
gucs := map[string]any{
"archive_command": "/bin/true",
"archive_mode": "on",
"checkpoint_completion_target": "0.9",
Expand All @@ -28,6 +61,10 @@ func DefaultGUCs() map[string]any {
"wal_log_hints": "on",
"wal_sender_timeout": "5s",
}
if needsOutputPluginLibraries(version) {
gucs["output_plugin_libraries"] = "pgoutput, test_decoding, spock_output"
}
return gucs
}

func SpockDefaultGUCs() map[string]any {
Expand Down
31 changes: 30 additions & 1 deletion server/internal/postgres/gucs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,40 @@ import (

"github.com/stretchr/testify/assert"

"github.com/pgEdge/control-plane/server/internal/ds"
"github.com/pgEdge/control-plane/server/internal/postgres"
)

func TestDefaultGUCs(t *testing.T) {
assert.Equal(t, "scram-sha-256", postgres.DefaultGUCs()["password_encryption"])
assert.Equal(t, "scram-sha-256", postgres.DefaultGUCs(nil)["password_encryption"])
}

func TestDefaultGUCsOutputPluginLibraries(t *testing.T) {
for _, tc := range []struct {
name string
version *ds.PgEdgeVersion
expectedPresent bool
}{
{name: "nil version", version: nil, expectedPresent: false},
{name: "pg16 below gate", version: ds.MustParsePgEdgeVersion("16.14", "4"), expectedPresent: false},
{name: "pg16 at gate", version: ds.MustParsePgEdgeVersion("16.15", "4"), expectedPresent: true},
{name: "pg16 above gate", version: ds.MustParsePgEdgeVersion("16.16", "4"), expectedPresent: true},
{name: "pg17 below gate", version: ds.MustParsePgEdgeVersion("17.10", "4"), expectedPresent: false},
{name: "pg17 at gate", version: ds.MustParsePgEdgeVersion("17.11", "4"), expectedPresent: true},
{name: "pg18 below gate", version: ds.MustParsePgEdgeVersion("18.4", "4"), expectedPresent: false},
{name: "pg18 at gate", version: ds.MustParsePgEdgeVersion("18.5", "4"), expectedPresent: true},
{name: "future major", version: ds.MustParsePgEdgeVersion("19.0", "4"), expectedPresent: true},
{name: "older major", version: ds.MustParsePgEdgeVersion("15.10", "4"), expectedPresent: false},
} {
t.Run(tc.name, func(t *testing.T) {
gucs := postgres.DefaultGUCs(tc.version)
value, ok := gucs["output_plugin_libraries"]
assert.Equal(t, tc.expectedPresent, ok)
if tc.expectedPresent {
assert.Equal(t, "pgoutput, test_decoding, spock_output", value)
}
})
}
}

func TestDefaultTunableGUCs(t *testing.T) {
Expand Down