Skip to content

Commit 979c44c

Browse files
committed
Merge pull request sorintlab#568 from prabhu43/restart-pg-for-necessary-params
Restart postgres if required when updating pgParameters
2 parents 47701ed + b62fc6e commit 979c44c

File tree

9 files changed

+283
-2
lines changed

9 files changed

+283
-2
lines changed

cmd/keeper/cmd/keeper.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1547,6 +1547,7 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) {
15471547
}
15481548

15491549
needsReload := false
1550+
changedParams := pgParameters.Diff(pgm.CurParameters())
15501551

15511552
if !pgParameters.Equals(pgm.CurParameters()) {
15521553
log.Infow("postgres parameters changed, reloading postgres instance")
@@ -1588,6 +1589,23 @@ func (p *PostgresKeeper) postgresKeeperSM(pctx context.Context) {
15881589
if err := pgm.Reload(); err != nil {
15891590
log.Errorw("failed to reload postgres instance", err)
15901591
}
1592+
1593+
clusterSpec := cd.Cluster.DefSpec()
1594+
automaticPgRestartEnabled := *clusterSpec.AutomaticPgRestart
1595+
1596+
if automaticPgRestartEnabled {
1597+
needsRestart, err := pgm.IsRestartRequired(changedParams)
1598+
if err != nil {
1599+
log.Errorw("Failed to checked if restart is required", err)
1600+
}
1601+
1602+
if needsRestart {
1603+
log.Infow("Restarting postgres")
1604+
if err := pgm.Restart(true); err != nil {
1605+
log.Errorw("Failed to restart postgres instance", err)
1606+
}
1607+
}
1608+
}
15911609
}
15921610

15931611
// If we are here, then all went well and we can update the db generation and save it locally

doc/cluster_spec.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ Some options in a running cluster specification can be changed to update the des
3636
| pitrConfig | configuration for initMode of type "pitr" | if initMode is "pitr" | PITRConfig | |
3737
| standbyConfig | standby config when the cluster is a standby cluster | if role is "standby" | StandbyConfig | |
3838
| pgParameters | a map containing the postgres server parameters and their values. The parameters value don't have to be quoted and single quotes don't have to be doubled since this is already done by the keeper when writing the postgresql.conf file | no | map[string]string | |
39-
| pgHBA | a list containing additional pg_hba.conf entries. They will be added to the pg_hba.conf generated by stolon. **NOTE**: these lines aren't validated so if some of them are wrong postgres will refuse to start or, on reload, will log a warning and ignore the updated pg_hba.conf file | no | []string | null. Will use the default behiavior of accepting connections from all hosts for all dbs and users with md5 password authentication |
39+
| pgHBA | a list containing additional pg_hba.conf entries. They will be added to the pg_hba.conf generated by stolon. **NOTE**: these lines aren't validated so if some of them are wrong postgres will refuse to start or, on reload, will log a warning and ignore the updated pg_hba.conf file | no | []string | null. Will use the default behiavior of accepting connections from all hosts for all dbs and users with md5 password authentication |
40+
| automaticPgRestart | restart postgres automatically after changing the pgParameters that requires restart. Refer `pending_restart` in [pg_settings](https://www.postgresql.org/docs/9.5/static/view-pg-settings.html) | no | bool | false |
4041

4142
#### ExistingConfig
4243

doc/postgres_parameters.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,7 @@ To disable this behavior just set `mergePgParameters` to false in the cluster sp
4848
Since postgresql.auto.conf overrides postgresql.conf parameters, changing some of them with ALTER SYSTEM could break the cluster (parameters managed by stolon could be overridden) and make pg parameters different between the instances.
4949

5050
To avoid this stolon disables the execution of ALTER SYSTEM commands making postgresql.auto.conf a symlink to /dev/null. When an ALTER SYSTEM command is executed it'll return an error.
51+
52+
## Restart postgres on changing some pg parameters
53+
54+
There are some pg parameters which requires postgres restart to take effect after changing. For example, changing `max_connections` will not take effect till the underlying postgres is restarted. This is disabled by default and can be enabled using the clusterSpecification `automaticPgRestart`. This is achieved using `pending_restart` in [pg_settings](https://www.postgresql.org/docs/9.5/static/view-pg-settings.html) for postgres 9.5 and above and the `context` column of `pg_settings` for lower versions (<= 9.4).

internal/cluster/cluster.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ const (
6969
DefaultMergePGParameter = true
7070
DefaultRole ClusterRole = ClusterRoleMaster
7171
DefaultSUReplAccess SUReplAccessMode = SUReplAccessAll
72+
DefaultAutomaticPgRestart = false
7273
)
7374

7475
const (
@@ -283,6 +284,8 @@ type ClusterSpec struct {
283284
// Additional pg_hba.conf entries
284285
// we don't set omitempty since we want to distinguish between null or empty slice
285286
PGHBA []string `json:"pgHBA"`
287+
// Enable automatic pg restart when pg parameters that requires restart changes
288+
AutomaticPgRestart *bool `json:"automaticPgRestart"`
286289
}
287290

288291
type ClusterStatus struct {
@@ -391,6 +394,9 @@ func (os *ClusterSpec) WithDefaults() *ClusterSpec {
391394
v := DefaultRole
392395
s.Role = &v
393396
}
397+
if s.AutomaticPgRestart == nil {
398+
s.AutomaticPgRestart = BoolP(DefaultAutomaticPgRestart)
399+
}
394400
return s
395401
}
396402

internal/common/common.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,23 @@ func (s Parameters) Equals(is Parameters) bool {
7373
return reflect.DeepEqual(s, is)
7474
}
7575

76+
// Diff returns the list of pgParameters changed(newly added, existing deleted and value changed)
77+
func (s Parameters) Diff(newParams Parameters) []string {
78+
var changedParams []string
79+
for k, v := range newParams {
80+
if val, ok := s[k]; !ok || v != val {
81+
changedParams = append(changedParams, k)
82+
}
83+
}
84+
85+
for k := range s {
86+
if _, ok := newParams[k]; !ok {
87+
changedParams = append(changedParams, k)
88+
}
89+
}
90+
return changedParams
91+
}
92+
7693
// WriteFileAtomicFunc atomically writes a file, it achieves this by creating a
7794
// temporary file and then moving it. writeFunc is the func that will write
7895
// data to the file.

internal/common/common_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
// Copyright 2018 Sorint.lab
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package common_test
16+
17+
import (
18+
"testing"
19+
20+
"github.com/sorintlab/stolon/internal/common"
21+
"github.com/sorintlab/stolon/internal/util"
22+
)
23+
24+
func TestDiffReturnsChangedParams(t *testing.T) {
25+
var curParams common.Parameters = map[string]string{
26+
"max_connections": "100",
27+
"shared_buffers": "10MB",
28+
"huge": "off",
29+
}
30+
31+
var newParams common.Parameters = map[string]string{
32+
"max_connections": "200",
33+
"shared_buffers": "10MB",
34+
"work_mem": "4MB",
35+
}
36+
37+
expectedDiff := []string{"max_connections", "huge", "work_mem"}
38+
39+
diff := curParams.Diff(newParams)
40+
41+
if !util.CompareStringSliceNoOrder(expectedDiff, diff) {
42+
t.Errorf("Expected diff is %v, but got %v", expectedDiff, diff)
43+
}
44+
}

internal/postgresql/postgresql.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -924,3 +924,20 @@ func (p *Manager) OlderWalFile() (string, error) {
924924

925925
return "", nil
926926
}
927+
928+
// IsRestartRequired returns if a postgres restart is necessary
929+
func (p *Manager) IsRestartRequired(changedParams []string) (bool, error) {
930+
maj, min, err := p.BinaryVersion()
931+
if err != nil {
932+
return false, fmt.Errorf("Error fetching pg version for checking IsRestartRequired, %v", err)
933+
}
934+
935+
ctx, cancel := context.WithTimeout(context.Background(), p.requestTimeout)
936+
defer cancel()
937+
938+
if maj == 9 && min < 5 {
939+
return isRestartRequiredUsingPgSettingsContext(ctx, p.localConnParams, changedParams)
940+
} else {
941+
return isRestartRequiredUsingPendingRestart(ctx, p.localConnParams)
942+
}
943+
}

internal/postgresql/utils.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ import (
2727

2828
"os"
2929

30-
_ "github.com/lib/pq"
30+
"github.com/lib/pq"
3131
)
3232

3333
const (
@@ -436,6 +436,56 @@ func getConfigFilePGParameters(ctx context.Context, connParams ConnParams) (comm
436436
return pgParameters, nil
437437
}
438438

439+
func isRestartRequiredUsingPendingRestart(ctx context.Context, connParams ConnParams) (bool, error) {
440+
isRestartRequired := false
441+
db, err := sql.Open("postgres", connParams.ConnString())
442+
if err != nil {
443+
return isRestartRequired, err
444+
}
445+
defer db.Close()
446+
447+
rows, err := query(ctx, db, "select count(*) > 0 from pg_settings where pending_restart;")
448+
if err != nil {
449+
return isRestartRequired, err
450+
}
451+
defer rows.Close()
452+
if rows.Next() {
453+
if err := rows.Scan(&isRestartRequired); err != nil {
454+
return isRestartRequired, err
455+
}
456+
}
457+
458+
return isRestartRequired, nil
459+
}
460+
461+
func isRestartRequiredUsingPgSettingsContext(ctx context.Context, connParams ConnParams, changedParams []string) (bool, error) {
462+
isRestartRequired := false
463+
db, err := sql.Open("postgres", connParams.ConnString())
464+
if err != nil {
465+
return isRestartRequired, err
466+
}
467+
defer db.Close()
468+
469+
stmt, err := db.Prepare("select count(*) > 0 from pg_settings where context = 'postmaster' and name = ANY($1)")
470+
471+
if err != nil {
472+
return false, err
473+
}
474+
475+
rows, err := stmt.Query(pq.Array(changedParams))
476+
if err != nil {
477+
return isRestartRequired, err
478+
}
479+
defer rows.Close()
480+
if rows.Next() {
481+
if err := rows.Scan(&isRestartRequired); err != nil {
482+
return isRestartRequired, err
483+
}
484+
}
485+
486+
return isRestartRequired, nil
487+
}
488+
439489
func ParseBinaryVersion(v string) (int, int, error) {
440490
// extact version (removing beta*, rc* etc...)
441491
regex, err := regexp.Compile(`.* \(PostgreSQL\) ([0-9\.]+).*`)

tests/integration/config_test.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,3 +462,127 @@ func TestAdditionalReplicationSlots(t *testing.T) {
462462
t.Fatalf("unexpected err: %v", err)
463463
}
464464
}
465+
466+
func TestAutomaticPgRestart(t *testing.T) {
467+
t.Parallel()
468+
469+
dir, err := ioutil.TempDir("", "")
470+
if err != nil {
471+
t.Fatalf("unexpected err: %v", err)
472+
}
473+
defer os.RemoveAll(dir)
474+
475+
tstore, err := NewTestStore(t, dir)
476+
if err != nil {
477+
t.Fatalf("unexpected err: %v", err)
478+
}
479+
if err := tstore.Start(); err != nil {
480+
t.Fatalf("unexpected err: %v", err)
481+
}
482+
if err := tstore.WaitUp(10 * time.Second); err != nil {
483+
t.Fatalf("error waiting on store up: %v", err)
484+
}
485+
storeEndpoints := fmt.Sprintf("%s:%s", tstore.listenAddress, tstore.port)
486+
defer tstore.Stop()
487+
488+
clusterName := uuid.NewV4().String()
489+
490+
storePath := filepath.Join(common.StorePrefix, clusterName)
491+
492+
sm := store.NewKVBackedStore(tstore.store, storePath)
493+
automaticPgRestart := true
494+
pgParameters := map[string]string{"max_connections": "100"}
495+
496+
initialClusterSpec := &cluster.ClusterSpec{
497+
InitMode: cluster.ClusterInitModeP(cluster.ClusterInitModeNew),
498+
AutomaticPgRestart: &automaticPgRestart,
499+
PGParameters: pgParameters,
500+
}
501+
502+
initialClusterSpecFile, err := writeClusterSpec(dir, initialClusterSpec)
503+
if err != nil {
504+
t.Fatalf("unexpected err: %v", err)
505+
}
506+
ts, err := NewTestSentinel(t, dir, clusterName, tstore.storeBackend, storeEndpoints, fmt.Sprintf("--initial-cluster-spec=%s", initialClusterSpecFile))
507+
if err != nil {
508+
t.Fatalf("unexpected err: %v", err)
509+
}
510+
if err := ts.Start(); err != nil {
511+
t.Fatalf("unexpected err: %v", err)
512+
}
513+
defer ts.Stop()
514+
515+
tk, err := NewTestKeeper(t, dir, clusterName, pgSUUsername, pgSUPassword, pgReplUsername, pgReplPassword, tstore.storeBackend, storeEndpoints)
516+
if err != nil {
517+
t.Fatalf("unexpected err: %v", err)
518+
}
519+
if err := tk.Start(); err != nil {
520+
t.Fatalf("unexpected err: %v", err)
521+
}
522+
defer tk.Stop()
523+
524+
if err := WaitClusterPhase(sm, cluster.ClusterPhaseNormal, 60*time.Second); err != nil {
525+
t.Fatalf("unexpected err: %v", err)
526+
}
527+
if err := tk.WaitDBUp(60 * time.Second); err != nil {
528+
t.Fatalf("unexpected err: %v", err)
529+
}
530+
531+
err = StolonCtl(clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "max_connections": "150" } }`)
532+
if err != nil {
533+
t.Fatalf("unexpected err: %v", err)
534+
}
535+
536+
// Wait for restart to happen
537+
time.Sleep(10 * time.Second)
538+
539+
rows, err := tk.Query("select setting from pg_settings where name = 'max_connections'")
540+
if err != nil {
541+
t.Fatalf("unexpected err: %v", err)
542+
}
543+
defer rows.Close()
544+
545+
if rows.Next() {
546+
var maxConnections int
547+
err = rows.Scan(&maxConnections)
548+
if err != nil {
549+
t.Fatalf("unexpected err: %v", err)
550+
}
551+
552+
if maxConnections != 150 {
553+
t.Errorf("expected max_connections %d is not equal to actual %d", 150, maxConnections)
554+
}
555+
}
556+
557+
// Allow users to opt out
558+
err = StolonCtl(clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "automaticPgRestart" : false }`)
559+
if err != nil {
560+
t.Fatalf("unexpected err: %v", err)
561+
}
562+
563+
err = StolonCtl(clusterName, tstore.storeBackend, storeEndpoints, "update", "--patch", `{ "pgParameters" : { "max_connections": "200" } }`)
564+
if err != nil {
565+
t.Fatalf("unexpected err: %v", err)
566+
}
567+
568+
// Restart should not happen, but waiting in case it restarts
569+
time.Sleep(10 * time.Second)
570+
571+
rows, err = tk.Query("select setting from pg_settings where name = 'max_connections'")
572+
if err != nil {
573+
t.Fatalf("unexpected err: %v", err)
574+
}
575+
defer rows.Close()
576+
577+
if rows.Next() {
578+
var maxConnections int
579+
err = rows.Scan(&maxConnections)
580+
if err != nil {
581+
t.Fatalf("unexpected err: %v", err)
582+
}
583+
584+
if maxConnections != 150 {
585+
t.Errorf("expected max_connections %d is not equal to actual %d", 150, maxConnections)
586+
}
587+
}
588+
}

0 commit comments

Comments
 (0)