Skip to content

Commit bb6c06a

Browse files
committed
Merge pull request sorintlab#450 from sgotti/keeper_improve_wal_level_selection
keeper: improve wal_level selection
2 parents 717a7c2 + 616c08b commit bb6c06a

File tree

5 files changed

+169
-7
lines changed

5 files changed

+169
-7
lines changed

cmd/keeper/cmd/keeper.go

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,6 @@ func init() {
122122

123123
var managedPGParameters = []string{
124124
"unix_socket_directories",
125-
"wal_level",
126125
"wal_keep_segments",
127126
"hot_standby",
128127
"listen_addresses",
@@ -166,10 +165,47 @@ func readPasswordFromFile(filepath string) (string, error) {
166165
return strings.TrimSpace(string(pwBytes)), nil
167166
}
168167

169-
func (p *PostgresKeeper) mandatoryPGParameters() common.Parameters {
168+
// walLevel returns the wal_level value to use.
169+
// if there's an user provided wal_level pg parameters and if its value is
170+
// "logical" then returns it, otherwise returns the default ("hot_standby" for
171+
// pg < 9.6 or "replica" for pg >= 9.6).
172+
func (p *PostgresKeeper) walLevel(db *cluster.DB) string {
173+
var additionalValidWalLevels = []string{
174+
"logical", // pg >= 10
175+
}
176+
177+
maj, min, err := p.pgm.BinaryVersion()
178+
if err != nil {
179+
// in case we fail to parse the binary version then log it and just use "hot_standby" that works for all versions
180+
log.Warnf("failed to get postgres binary version: %v", err)
181+
return "hot_standby"
182+
}
183+
184+
// set default wal_level
185+
walLevel := "hot_standby"
186+
if maj == 9 {
187+
if min >= 6 {
188+
walLevel = "replica"
189+
}
190+
} else if maj >= 10 {
191+
walLevel = "replica"
192+
}
193+
194+
if db.Spec.PGParameters != nil {
195+
if l, ok := db.Spec.PGParameters["wal_level"]; ok {
196+
if util.StringInSlice(additionalValidWalLevels, l) {
197+
walLevel = l
198+
}
199+
}
200+
}
201+
202+
return walLevel
203+
}
204+
205+
func (p *PostgresKeeper) mandatoryPGParameters(db *cluster.DB) common.Parameters {
170206
return common.Parameters{
171207
"unix_socket_directories": common.PgUnixSocketDirectories,
172-
"wal_level": "hot_standby",
208+
"wal_level": p.walLevel(db),
173209
"wal_keep_segments": "8",
174210
"hot_standby": "on",
175211
}
@@ -248,7 +284,7 @@ func (p *PostgresKeeper) createPGParameters(db *cluster.DB) common.Parameters {
248284
}
249285

250286
// Add/Replace mandatory PGParameters
251-
for k, v := range p.mandatoryPGParameters() {
287+
for k, v := range p.mandatoryPGParameters(db) {
252288
parameters[k] = v
253289
}
254290

doc/postgres_parameters.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ These parameters, if defined in the cluster specification, will be ignored since
1414
listen_addresses
1515
port
1616
unix_socket_directories
17-
wal_level
1817
wal_keep_segments
1918
wal_log_hints
2019
hot_standby
@@ -23,11 +22,18 @@ max_wal_senders
2322
synchronous_standby_names
2423
```
2524

25+
## Special cases
26+
27+
### wal_level
28+
29+
since stolon requires a `wal_level` value of at least `replica` (or `hot_standby` for pg < 9.6) if you leave it unspecificed in the `pgParameters` or if you specify a wrong `wal_level` or a value lesser than `replica` or `hot_standby` (like `minimal`) it'll be overridden by the minimal working value (`replica` or `hot_standby`).
30+
31+
i.e. if you want to also save logical replication information in the wal files you can specify a `wal_level` set to `logical`.
32+
2633
## Parameters validity checks
2734

2835
Actually stolon doesn't do any check on the provided configurations, so, if the provided parameters are wrong this won't create problems at instance reload (just some warning in the postgresql logs) but at the next instance restart, it'll probably fail making the instance not available (thus triggering failover if it's the master or other changes in the clusterview).
2936

30-
3137
## Initialization parameters
3238

3339
When [initializing the cluster](initialization.md), by default, stolon will merge in the cluster spec the parameters that the instance had at the end of the initialization, practically:

pkg/postgresql/utils.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ func getConfigFilePGParameters(ctx context.Context, connParams ConnParams) (comm
435435

436436
func ParseBinaryVersion(v string) (int, int, error) {
437437
// extact version (removing beta*, rc* etc...)
438-
regex, err := regexp.Compile(`.* \(PostgreSQL\) ([0-9\.]+).*$`)
438+
regex, err := regexp.Compile(`.* \(PostgreSQL\) ([0-9\.]+).*`)
439439
if err != nil {
440440
return 0, 0, err
441441
}

pkg/postgresql/utils_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,11 @@ func TestParseBinaryVersion(t *testing.T) {
140140
maj: 9,
141141
min: 5,
142142
},
143+
{
144+
in: "postgres (PostgreSQL) 9.6.7\n",
145+
maj: 9,
146+
min: 6,
147+
},
143148
{
144149
in: "postgres (PostgreSQL) 10beta1",
145150
maj: 10,

tests/integration/config_test.go

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,121 @@ func TestServerParameters(t *testing.T) {
126126
}
127127
}
128128

129+
func TestWalLevel(t *testing.T) {
130+
t.Parallel()
131+
132+
dir, err := ioutil.TempDir("", "")
133+
if err != nil {
134+
t.Fatalf("unexpected err: %v", err)
135+
}
136+
defer os.RemoveAll(dir)
137+
138+
tstore, err := NewTestStore(t, dir)
139+
if err != nil {
140+
t.Fatalf("unexpected err: %v", err)
141+
}
142+
if err := tstore.Start(); err != nil {
143+
t.Fatalf("unexpected err: %v", err)
144+
}
145+
if err := tstore.WaitUp(10 * time.Second); err != nil {
146+
t.Fatalf("error waiting on store up: %v", err)
147+
}
148+
storeEndpoints := fmt.Sprintf("%s:%s", tstore.listenAddress, tstore.port)
149+
defer tstore.Stop()
150+
151+
clusterName := uuid.NewV4().String()
152+
153+
storePath := filepath.Join(common.StorePrefix, clusterName)
154+
155+
sm := store.NewKVBackedStore(tstore.store, storePath)
156+
157+
initialClusterSpec := &cluster.ClusterSpec{
158+
InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew),
159+
SleepInterval: &cluster.Duration{Duration: 2 * time.Second},
160+
FailInterval: &cluster.Duration{Duration: 5 * time.Second},
161+
ConvergenceTimeout: &cluster.Duration{Duration: 30 * time.Second},
162+
}
163+
initialClusterSpecFile, err := writeClusterSpec(dir, initialClusterSpec)
164+
if err != nil {
165+
t.Fatalf("unexpected err: %v", err)
166+
}
167+
ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile))
168+
if err != nil {
169+
t.Fatalf("unexpected err: %v", err)
170+
}
171+
if err := ts.Start(); err != nil {
172+
t.Fatalf("unexpected err: %v", err)
173+
}
174+
tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints)
175+
if err != nil {
176+
t.Fatalf("unexpected err: %v", err)
177+
}
178+
if err := tk.Start(); err != nil {
179+
t.Fatalf("unexpected err: %v", err)
180+
}
181+
182+
if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil {
183+
t.Fatalf("unexpected err: %v", err)
184+
}
185+
if err := tk.WaitDBUp(60 * time.Second); err != nil {
186+
t.Fatalf("unexpected err: %v", err)
187+
}
188+
189+
// "archive" isn't an accepted wal_level
190+
err = StolonCtl(clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_level": "archive" } }`)
191+
if err != nil {
192+
t.Fatalf("unexpected err: %v", err)
193+
}
194+
195+
if err := tk.cmd.ExpectTimeout("postgres parameters not changed", 30*time.Second); err != nil {
196+
t.Fatalf("unexpected err: %v", err)
197+
}
198+
199+
tk.Stop()
200+
if err := tk.Start(); err != nil {
201+
t.Fatalf("unexpected err: %v", err)
202+
}
203+
if err := tk.WaitDBUp(60 * time.Second); err != nil {
204+
t.Fatalf("unexpected err: %v", err)
205+
}
206+
207+
pgParameters, err := tk.GetPGParameters()
208+
if err != nil {
209+
t.Fatalf("unexpected err: %v", err)
210+
}
211+
walLevel := pgParameters["wal_level"]
212+
if walLevel != "replica" && walLevel != "hot_standby" {
213+
t.Fatalf("unexpected wal_level value: %q", walLevel)
214+
}
215+
216+
// "logical" is an accepted wal_level
217+
err = StolonCtl(clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "wal_level": "logical" } }`)
218+
if err != nil {
219+
t.Fatalf("unexpected err: %v", err)
220+
}
221+
222+
if err := tk.cmd.ExpectTimeout("postgres parameters changed, reloading postgres instance", 30*time.Second); err != nil {
223+
t.Fatalf("unexpected err: %v", err)
224+
}
225+
226+
tk.Stop()
227+
if err := tk.Start(); err != nil {
228+
t.Fatalf("unexpected err: %v", err)
229+
}
230+
if err := tk.WaitDBUp(60 * time.Second); err != nil {
231+
t.Fatalf("unexpected err: %v", err)
232+
}
233+
234+
pgParameters, err = tk.GetPGParameters()
235+
if err != nil {
236+
t.Fatalf("unexpected err: %v", err)
237+
}
238+
walLevel = pgParameters["wal_level"]
239+
if walLevel != "logical" {
240+
t.Fatalf("unexpected wal_level value: %q", walLevel)
241+
}
242+
}
243+
129244
func TestAlterSystem(t *testing.T) {
130245
t.Parallel()
131246

0 commit comments

Comments
 (0)