From 8da1336146f9fddaa4c731c4a57d8e8783a6f041 Mon Sep 17 00:00:00 2001 From: Jason Lynch Date: Wed, 3 Jun 2026 10:42:26 -0400 Subject: [PATCH] fix: update global patroni params for systemd Patroni disallows setting some parameters through the Patroni config file after the cluster is created. Instead, they must be set via the dynamic config API. This commit adds functionality to the systemd Patroni config implementation to patch the dynamic config if the Patroni API is up and if the API reports that it is the primary instance. This commit only affects systemd clusters. I will implement this for Swarm clusters in a subsequent commit by migrating Swarm to use the common Patroni config resource. PLAT-610 --- e2e/patroni_global_params_test.go | 101 +++++++++++ .../enable_fast_basebackup.yaml | 1 + .../in-place_restore.yaml | 1 + .../minimal_swarm.yaml | 1 + .../minimal_systemd.yaml | 1 + .../user_pg_hba_pg_ident_and_scram.yaml | 1 + .../with_backup_config.yaml | 1 + .../with_restore_config.yaml | 1 + .../orchestrator/common/patroni_config.go | 169 ++++++++++++++---- .../common/patroni_config_generator.go | 4 + .../common/patroni_config_generator_test.go | 6 + .../orchestrator/systemd/patroni_config.go | 50 +++--- server/internal/patroni/client.go | 6 +- server/internal/patroni/config.go | 57 +++++- 14 files changed, 341 insertions(+), 59 deletions(-) create mode 100644 e2e/patroni_global_params_test.go diff --git a/e2e/patroni_global_params_test.go b/e2e/patroni_global_params_test.go new file mode 100644 index 00000000..9f5fb17f --- /dev/null +++ b/e2e/patroni_global_params_test.go @@ -0,0 +1,101 @@ +//go:build e2e_test + +package e2e + +import ( + "testing" + + "github.com/jackc/pgx/v5" + api "github.com/pgEdge/control-plane/api/apiv1/gen/control_plane" + "github.com/stretchr/testify/require" +) + +func TestUpdatePatroniGlobalParams(t *testing.T) { + t.Parallel() + + // I'm intentionally not adding a helper function for this check because + // this condition will be very temporary. + if fixture.orchestrator != "systemd" { + t.Skip("patroni global parameter update is currently unsupported in non-systemd clusters") + } + + host1 := fixture.HostIDs()[0] + + username := "admin" + password := "password" + + tLog(t, "creating database") + + ctx := t.Context() + db := fixture.NewDatabaseFixture(ctx, t, &api.CreateDatabaseRequest{ + Spec: &api.DatabaseSpec{ + DatabaseName: "test_global_params", + DatabaseUsers: []*api.DatabaseUserSpec{ + { + Username: username, + Password: pointerTo(password), + DbOwner: pointerTo(true), + Attributes: []string{"LOGIN", "SUPERUSER"}, + }, + }, + Port: pointerTo(0), + PatroniPort: pointerTo(0), + Nodes: []*api.DatabaseNodeSpec{ + { + Name: "n1", + HostIds: []api.Identifier{api.Identifier(host1)}, + }, + }, + }, + }) + + tLog(t, "querying original max connections") + + opts := ConnectionOptions{ + Username: username, + Password: password, + } + var originalMaxConnections int + db.WithConnection(ctx, opts, t, func(conn *pgx.Conn) { + require.NoError(t, conn.QueryRow(ctx, `SELECT setting::integer FROM pg_settings WHERE name = 'max_connections';`).Scan(&originalMaxConnections)) + }) + + tLogf(t, "got max_connections=%d", originalMaxConnections) + tLog(t, "updating database to change max connections") + + err := db.Update(ctx, UpdateOptions{ + Spec: &api.DatabaseSpec{ + DatabaseName: "test_global_params", + DatabaseUsers: []*api.DatabaseUserSpec{ + { + Username: username, + DbOwner: pointerTo(true), + Attributes: []string{"LOGIN", "SUPERUSER"}, + }, + }, + Port: pointerTo(0), + PatroniPort: pointerTo(0), + PostgresqlConf: map[string]any{ + "max_connections": originalMaxConnections + 1, + }, + Nodes: []*api.DatabaseNodeSpec{ + { + Name: "n1", + HostIds: []api.Identifier{api.Identifier(host1)}, + }, + }, + }, + }) + require.NoError(t, err) + + tLog(t, "querying new max connections") + + var newMaxConnections int + db.WithConnection(ctx, opts, t, func(conn *pgx.Conn) { + require.NoError(t, conn.QueryRow(ctx, `SELECT setting::integer FROM pg_settings WHERE name = 'max_connections';`).Scan(&newMaxConnections)) + }) + + tLogf(t, "got max_connections=%d", newMaxConnections) + + require.Equal(t, originalMaxConnections+1, newMaxConnections) +} diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/enable_fast_basebackup.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/enable_fast_basebackup.yaml index 4845166c..dc77d1bb 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/enable_fast_basebackup.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/enable_fast_basebackup.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/in-place_restore.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/in-place_restore.yaml index b27294c4..e22be2cc 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/in-place_restore.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/in-place_restore.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_swarm.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_swarm.yaml index 9e17bd3a..5cbd7337 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_swarm.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_swarm.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: true postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_systemd.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_systemd.yaml index 36c7dd75..43e5cd91 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_systemd.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/minimal_systemd.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/user_pg_hba_pg_ident_and_scram.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/user_pg_hba_pg_ident_and_scram.yaml index 76ea7dd4..caf8c437 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/user_pg_hba_pg_ident_and_scram.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/user_pg_hba_pg_ident_and_scram.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_backup_config.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_backup_config.yaml index 5e0a728e..c344407e 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_backup_config.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_backup_config.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_restore_config.yaml b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_restore_config.yaml index aa4b4937..0e7d5b22 100644 --- a/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_restore_config.yaml +++ b/server/internal/orchestrator/common/golden_test/TestPatroniConfigGenerator/with_restore_config.yaml @@ -13,6 +13,7 @@ bootstrap: loop_wait: 10 ttl: 30 retry_timeout: 10 + failsafe_mode: false postgresql: parameters: max_connections: 901 diff --git a/server/internal/orchestrator/common/patroni_config.go b/server/internal/orchestrator/common/patroni_config.go index dc267bcb..9af2451b 100644 --- a/server/internal/orchestrator/common/patroni_config.go +++ b/server/internal/orchestrator/common/patroni_config.go @@ -5,8 +5,10 @@ import ( "errors" "fmt" "path/filepath" + "time" "github.com/goccy/go-yaml" + "github.com/rs/zerolog" "github.com/samber/do" "github.com/spf13/afero" clientv3 "go.etcd.io/etcd/client/v3" @@ -74,11 +76,58 @@ func (c *PatroniConfig) Create( systemAddresses []string, extraHbaEntries []hba.Entry, ) error { - fs, err := do.Invoke[afero.Fs](rc.Injector) + _, err := c.create(ctx, rc, systemAddresses, extraHbaEntries) + return err +} + +func (c *PatroniConfig) Update( + ctx context.Context, + rc *resource.Context, + systemAddresses []string, + extraHbaEntries []hba.Entry, + reload func(ctx context.Context, rc *resource.Context, wait time.Duration) error, +) error { + logger, err := do.Invoke[zerolog.Logger](rc.Injector) if err != nil { return err } - etcdClient, err := do.Invoke[*clientv3.Client](rc.Injector) + + cfg, err := c.create(ctx, rc, systemAddresses, extraHbaEntries) + if err != nil { + return err + } + + wait := patroni.DefaultLoopWaitSeconds * time.Second + if client := c.client(rc); client != nil { + w, isPrimary := c.getStatusInfo(ctx, client) + if w > 0 { + wait = w + } + if isPrimary && cfg.Bootstrap != nil && cfg.Bootstrap.DCS != nil { + _, err := client.PatchDynamicConfig(ctx, cfg.Bootstrap.DCS.ToDynamicConfig()) + if err != nil { + logger.Warn(). + Str("database_id", c.Generator.DatabaseID). + Str("instance_id", c.InstanceID). + Err(err). + Msg("failed to patch dynamic config") + } + } + } + + // We intentionally leave the reload implementation up to the caller rather + // than use the Patroni API's reload method. A process signal-based reload + // has the advantage that it will work regardless of whether the Patroni API + // is healthy. + if err := reload(ctx, rc, wait); err != nil { + return fmt.Errorf("failed to trigger patroni reload: %w", err) + } + + return nil +} + +func (c *PatroniConfig) Delete(ctx context.Context, rc *resource.Context) error { + fs, err := do.Invoke[afero.Fs](rc.Injector) if err != nil { return err } @@ -88,62 +137,71 @@ func (c *PatroniConfig) Create( return fmt.Errorf("failed to get parent full path: %w", err) } + err = fs.Remove(filepath.Join(parentFullPath, "patroni.yaml")) + if errors.Is(err, afero.ErrFileNotFound) { + return nil + } else if err != nil { + return fmt.Errorf("failed to remove patroni.yaml: %w", err) + } + + return nil +} + +func (c *PatroniConfig) create( + ctx context.Context, + rc *resource.Context, + systemAddresses []string, + extraHbaEntries []hba.Entry, +) (*patroni.Config, error) { + fs, err := do.Invoke[afero.Fs](rc.Injector) + if err != nil { + return nil, err + } + etcdClient, err := do.Invoke[*clientv3.Client](rc.Injector) + if err != nil { + return nil, err + } + + parentFullPath, err := filesystem.DirResourceFullPath(rc, c.ParentID) + if err != nil { + return nil, fmt.Errorf("failed to get parent full path: %w", err) + } + etcdCreds, err := resource.FromContext[*EtcdCreds](rc, EtcdCredsIdentifier(c.InstanceID)) if err != nil { - return fmt.Errorf("failed to get etcd creds from state: %w", err) + return nil, fmt.Errorf("failed to get etcd creds from state: %w", err) } etcdHosts, err := patroni.EtcdHosts(ctx, etcdClient) if err != nil { - return fmt.Errorf("failed to get etcd hosts: %w", err) + return nil, fmt.Errorf("failed to get etcd hosts: %w", err) } enableFastBasebackup, err := c.isNewNode(rc) if err != nil { - return err + return nil, err } - config := c.Generator.Generate(etcdHosts, etcdCreds, GenerateOptions{ + cfg := c.Generator.Generate(etcdHosts, etcdCreds, GenerateOptions{ EnableFastBasebackup: enableFastBasebackup, SystemAddresses: systemAddresses, ExtraHbaEntries: extraHbaEntries, }) - content, err := yaml.Marshal(config) + content, err := yaml.Marshal(cfg) if err != nil { - return fmt.Errorf("failed to marshal patroni config: %w", err) + return nil, fmt.Errorf("failed to marshal patroni config: %w", err) } configPath := filepath.Join(parentFullPath, "patroni.yaml") if err := afero.WriteFile(fs, configPath, content, 0o600); err != nil { - return fmt.Errorf("failed to write %s: %w", configPath, err) + return nil, fmt.Errorf("failed to write %s: %w", configPath, err) } if err := fs.Chown(configPath, c.OwnerUID, c.OwnerGID); err != nil { - return fmt.Errorf("failed to change ownership for %s: %w", configPath, err) - } - - return nil -} - -func (c *PatroniConfig) Delete(ctx context.Context, rc *resource.Context) error { - fs, err := do.Invoke[afero.Fs](rc.Injector) - if err != nil { - return err - } - - parentFullPath, err := filesystem.DirResourceFullPath(rc, c.ParentID) - if err != nil { - return fmt.Errorf("failed to get parent full path: %w", err) - } - - err = fs.Remove(filepath.Join(parentFullPath, "patroni.yaml")) - if errors.Is(err, afero.ErrFileNotFound) { - return nil - } else if err != nil { - return fmt.Errorf("failed to remove patroni.yaml: %w", err) + return nil, fmt.Errorf("failed to change ownership for %s: %w", configPath, err) } - return nil + return cfg, nil } func (c *PatroniConfig) isNewNode(rc *resource.Context) (bool, error) { @@ -157,3 +215,50 @@ func (c *PatroniConfig) isNewNode(rc *resource.Context) (bool, error) { return false, nil } } + +func (c *PatroniConfig) client(rc *resource.Context) *patroni.Client { + // We're not using FromContext here to handle the case where the instance + // creation failed, but Patroni is still running. + data, ok := rc.State.Get(database.InstanceResourceIdentifier(c.InstanceID)) + if !ok { + return nil + } + instance, err := resource.ToResource[*database.InstanceResource](data) + if err == nil && instance.ConnectionInfo != nil { + return patroni.NewClient(instance.ConnectionInfo.PatroniURL(), nil) + } + + return nil +} + +func (c *PatroniConfig) getStatusInfo(ctx context.Context, client *patroni.Client) (time.Duration, bool) { + cfg, err := client.GetDynamicConfig(ctx) + if err != nil { + return 0, false + } + + var loopWait time.Duration + if cfg.LoopWait == nil { + loopWait = patroni.DefaultLoopWaitSeconds * time.Second + } else { + loopWait = time.Duration(*cfg.LoopWait) * time.Second + } + + wait := loopWait + status, err := client.GetInstanceStatus(ctx) + if err != nil { + return wait, false + } + if status.DCSLastSeen != nil { + lastSeen := time.Unix(*status.DCSLastSeen, 0) + lowerBound := time.Now().Add(-2 * loopWait) + upperBound := time.Now() + // Ignore last seen if clocks are very off in either direction + if lastSeen.After(lowerBound) && lastSeen.Before(upperBound) { + // Compute the time until the next run cycle + wait = time.Until(lastSeen.Add(loopWait)) + } + } + + return wait, status.IsPrimary() +} diff --git a/server/internal/orchestrator/common/patroni_config_generator.go b/server/internal/orchestrator/common/patroni_config_generator.go index 07913346..3f7dd182 100644 --- a/server/internal/orchestrator/common/patroni_config_generator.go +++ b/server/internal/orchestrator/common/patroni_config_generator.go @@ -48,6 +48,8 @@ type PatroniConfigGenerator struct { // this would be '1'. This is used to configure the Snowflake and LOLOR // extensions. NodeOrdinal int `json:"node_ordinal"` + // NodeSize is the number of instances in this Spock node. + NodeSize int `json:"node_size"` // OrchestratorParameters are additional parameters to be provided by the // orchestrator implementation. OrchestratorParameters map[string]any `json:"orchestrator_parameters,omitempty"` @@ -138,6 +140,7 @@ func NewPatroniConfigGenerator(opts PatroniConfigGeneratorOptions) *PatroniConfi MemoryBytes: memoryBytes, NodeName: opts.Instance.NodeName, NodeOrdinal: opts.Instance.NodeOrdinal, + NodeSize: opts.Instance.NodeSize, OrchestratorParameters: opts.OrchestratorParameters, PatroniPort: opts.PatroniPort, PostgresCertsDir: opts.Paths.Instance.PostgresCertificates(), @@ -230,6 +233,7 @@ func (p *PatroniConfigGenerator) bootstrap(dcsParameters map[string]any) *patron TTL: utils.PointerTo(30), LoopWait: utils.PointerTo(int(patroni.DefaultLoopWaitSeconds)), RetryTimeout: utils.PointerTo(10), + FailsafeMode: utils.PointerTo(p.NodeSize == 1), }, InitDB: utils.PointerTo([]string{"data-checksums"}), } diff --git a/server/internal/orchestrator/common/patroni_config_generator_test.go b/server/internal/orchestrator/common/patroni_config_generator_test.go index 2a99737f..7ac448ba 100644 --- a/server/internal/orchestrator/common/patroni_config_generator_test.go +++ b/server/internal/orchestrator/common/patroni_config_generator_test.go @@ -52,6 +52,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 1, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, }, @@ -109,6 +110,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 2, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, BackupConfig: &database.BackupConfig{}, @@ -167,6 +169,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 2, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, RestoreConfig: &database.RestoreConfig{}, @@ -225,6 +228,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 2, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, RestoreConfig: &database.RestoreConfig{}, @@ -284,6 +288,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 2, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, }, @@ -325,6 +330,7 @@ func TestPatroniConfigGenerator(t *testing.T) { DatabaseName: "app", NodeName: "n1", NodeOrdinal: 1, + NodeSize: 2, PgEdgeVersion: ds.MustParsePgEdgeVersion("18.1", "5.0.4"), ClusterSize: 3, }, diff --git a/server/internal/orchestrator/systemd/patroni_config.go b/server/internal/orchestrator/systemd/patroni_config.go index 0ac8a1b8..6f4d3ddd 100644 --- a/server/internal/orchestrator/systemd/patroni_config.go +++ b/server/internal/orchestrator/systemd/patroni_config.go @@ -10,7 +10,6 @@ import ( "github.com/pgEdge/control-plane/server/internal/ds" "github.com/pgEdge/control-plane/server/internal/host" "github.com/pgEdge/control-plane/server/internal/orchestrator/common" - "github.com/pgEdge/control-plane/server/internal/patroni" "github.com/pgEdge/control-plane/server/internal/resource" "github.com/pgEdge/control-plane/server/internal/utils" "github.com/samber/do" @@ -62,16 +61,37 @@ func (c *PatroniConfig) Refresh(ctx context.Context, rc *resource.Context) error } func (c *PatroniConfig) Create(ctx context.Context, rc *resource.Context) error { - hostSvc, err := do.Invoke[*host.Service](rc.Injector) + addresses, err := c.getAddresses(ctx, rc) + if err != nil { + return err + } + + return c.Base.Create(ctx, rc, addresses, nil) +} + +func (c *PatroniConfig) Update(ctx context.Context, rc *resource.Context) error { + addresses, err := c.getAddresses(ctx, rc) if err != nil { return err } + return c.Base.Update(ctx, rc, addresses, nil, c.signalReload) +} + +func (c *PatroniConfig) Delete(ctx context.Context, rc *resource.Context) error { + return c.Base.Delete(ctx, rc) +} + +func (c *PatroniConfig) getAddresses(ctx context.Context, rc *resource.Context) ([]string, error) { + hostSvc, err := do.Invoke[*host.Service](rc.Injector) + if err != nil { + return nil, err + } hosts, err := hostSvc.GetHosts(ctx, c.AllHostIDs) if err != nil { - return fmt.Errorf("failed to get hosts: %w", err) + return nil, fmt.Errorf("failed to get hosts: %w", err) } if len(hosts) != len(c.AllHostIDs) { - return fmt.Errorf("wrong number of hosts: expected %d, got %d", len(c.AllHostIDs), len(hosts)) + return nil, fmt.Errorf("wrong number of hosts: expected %d, got %d", len(c.AllHostIDs), len(hosts)) } addresses := ds.NewSet[string]() @@ -79,22 +99,10 @@ func (c *PatroniConfig) Create(ctx context.Context, rc *resource.Context) error addresses.Add(h.PeerAddresses...) } - return c.Base.Create(ctx, rc, addresses.ToSortedSlice(strings.Compare), nil) -} - -func (c *PatroniConfig) Update(ctx context.Context, rc *resource.Context) error { - if err := c.Create(ctx, rc); err != nil { - return err - } - - return c.signalReload(ctx, rc) + return addresses.ToSortedSlice(strings.Compare), nil } -func (c *PatroniConfig) Delete(ctx context.Context, rc *resource.Context) error { - return c.Base.Delete(ctx, rc) -} - -func (c *PatroniConfig) signalReload(ctx context.Context, rc *resource.Context) error { +func (c *PatroniConfig) signalReload(ctx context.Context, rc *resource.Context, wait time.Duration) error { client, err := do.Invoke[*Client](rc.Injector) if err != nil { return err @@ -110,8 +118,6 @@ func (c *PatroniConfig) signalReload(ctx context.Context, rc *resource.Context) if err := client.ReloadUnit(ctx, name); err != nil { return fmt.Errorf("failed to reload patroni: %w", err) } - // It can take up to loop_wait seconds for Patroni to reload the config. - // We'll want to update this code to read loop_wait from c.Base if we make - // loop_wait configurable. - return utils.SleepContext(ctx, patroni.DefaultLoopWaitSeconds*time.Second) + + return utils.SleepContext(ctx, wait) } diff --git a/server/internal/patroni/client.go b/server/internal/patroni/client.go index f8ad26b8..97504499 100644 --- a/server/internal/patroni/client.go +++ b/server/internal/patroni/client.go @@ -302,14 +302,14 @@ type DynamicStandbyClusterConfig struct { // An ordered list of methods that can be used to bootstrap standby leader // from the remote primary, can be different from the list defined in // PostgreSQL - CreateReplicaMethods *string `json:"create_replica_methods,omitempty"` + CreateReplicaMethods *[]string `json:"create_replica_methods,omitempty"` // Command to restore WAL records from the remote primary to nodes in a // standby cluster, can be different from the list defined in PostgreSQL RestoreCommand *string `json:"restore_command,omitempty"` // Cleanup command for standby leader ArchiveCleanupCommand *string `json:"archive_cleanup_command,omitempty"` // How long to wait before actually apply WAL records on a standby leader - RecoveryMinApplyDelay *string `json:"recovery_min_apply_delay,omitempty"` + RecoveryMinApplyDelay *int `json:"recovery_min_apply_delay,omitempty"` } type DynamicConfig struct { @@ -375,7 +375,7 @@ type DynamicConfig struct { // matching properties will cause a slot to be ignored. IgnoreSlots *[]IgnoreSlot `json:"ignore_slots,omitempty"` // Stops Patroni from making changes to the cluster. - Pause bool `json:"pause"` + Pause *bool `json:"pause,omitempty"` } type Switchover struct { diff --git a/server/internal/patroni/config.go b/server/internal/patroni/config.go index 0a63bde3..0c5c1d3e 100644 --- a/server/internal/patroni/config.go +++ b/server/internal/patroni/config.go @@ -70,6 +70,19 @@ type DCSPostgreSQL struct { PgIdent *[]string `json:"pg_ident,omitempty"` } +func (d *DCSPostgreSQL) ToDynamicConfig() *DynamicPostgreSQLConfig { + if d == nil { + return nil + } + return &DynamicPostgreSQLConfig{ + UsePgRewind: d.UsePgRewind, + UseSlots: d.UseSlots, + Parameters: d.Parameters, + PgHba: d.PgHba, + PgIdent: d.PgIdent, + } +} + type DCSStandbyCluster struct { Host *string `json:"host,omitempty"` Port *int `json:"port,omitempty"` @@ -80,6 +93,21 @@ type DCSStandbyCluster struct { RecoveryMinApplyDelay *int `json:"recovery_min_apply_delay,omitempty"` } +func (d *DCSStandbyCluster) ToDynamicConfig() *DynamicStandbyClusterConfig { + if d == nil { + return nil + } + return &DynamicStandbyClusterConfig{ + Host: d.Host, + Port: d.Port, + PrimarySlotName: d.PrimarySlotName, + CreateReplicaMethods: d.CreateReplicaMethods, + RestoreCommand: d.RestoreCommand, + ArchiveCleanupCommand: d.ArchiveCleanupCommand, + RecoveryMinApplyDelay: d.RecoveryMinApplyDelay, + } +} + type SlotType string const ( @@ -111,15 +139,40 @@ type DCS struct { PrimaryStopTimeout *int `json:"primary_stop_timeout,omitempty"` SynchronousMode *string `json:"synchronous_mode,omitempty"` SynchronousModeStrict *bool `json:"synchronous_mode_strict,omitempty"` - SynchronousModeCount *int `json:"synchronous_mode_count,omitempty"` + SynchronousNodeCount *int `json:"synchronous_node_count,omitempty"` FailsafeMode *bool `json:"failsafe_mode,omitempty"` Postgresql *DCSPostgreSQL `json:"postgresql,omitempty"` StandbyCluster *DCSStandbyCluster `json:"standby_cluster,omitempty"` - MemberSlotsTtl *string `json:"member_slots_ttl,omitempty"` + MemberSlotsTtl *int `json:"member_slots_ttl,omitempty"` Slots *map[string]Slot `json:"slots,omitempty"` IgnoreSlots *[]IgnoreSlot `json:"ignore_slots,omitempty"` } +func (d *DCS) ToDynamicConfig() *DynamicConfig { + if d == nil { + return nil + } + return &DynamicConfig{ + LoopWait: d.LoopWait, + TTL: d.TTL, + RetryTimeout: d.RetryTimeout, + MaximumLagOnFailover: d.MaximumLagOnFailover, + MaximumLagOnSyncNode: d.MaximumLagOnSyncNode, + MaxTimelinesHistory: d.MaxTimelinesHistory, + PrimaryStartTimeout: d.PrimaryStartTimeout, + PrimaryStopTimeout: d.PrimaryStopTimeout, + SynchronousMode: (*SynchronousMode)(d.SynchronousMode), + SynchronousModeStrict: d.SynchronousModeStrict, + SynchronousNodeCount: d.SynchronousNodeCount, + FailsafeMode: d.FailsafeMode, + MemberSlotsTtl: d.MemberSlotsTtl, + Slots: d.Slots, + IgnoreSlots: d.IgnoreSlots, + PostgreSQL: d.Postgresql.ToDynamicConfig(), + StandbyCluster: d.StandbyCluster.ToDynamicConfig(), + } +} + type BootstrapMethod string const (