Skip to content
Open
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
32 changes: 11 additions & 21 deletions cmd/sentinel/cmd/sentinel.go
Original file line number Diff line number Diff line change
Expand Up @@ -1266,27 +1266,6 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf
addedCount++
}

// If there're some missing standbys to reach
// MinSynchronousStandbys, keep previous sync standbys,
// also if not in a good state. In this way we have more
// possibilities to choose a sync standby to replace a
// failed master if they becoe healthy again
ac = int(*clusterSpec.MinSynchronousStandbys) - len(synchronousStandbys)
addedCount = 0
for _, db := range newcd.DBs {
if addedCount >= ac {
break
}
if _, ok := synchronousStandbys[db.UID]; ok {
continue
}
if _, ok := prevSynchronousStandbys[db.UID]; ok {
log.Infow("adding previous synchronous standby to reach MinSynchronousStandbys", "masterDB", masterDB.UID, "synchronousStandbyDB", db.UID, "keeper", db.Spec.KeeperUID)
synchronousStandbys[db.UID] = struct{}{}
addedCount++
}
}

// if some of the new synchronousStandbys are not inside
// the prevSynchronousStandbys then also add all
// the prevSynchronousStandbys. In this way when there's
Expand Down Expand Up @@ -1321,6 +1300,17 @@ func (s *Sentinel) updateCluster(cd *cluster.ClusterData, pis cluster.ProxiesInf
}

// If there're not enough real synchronous standbys add a fake synchronous standby because we have to be strict and make the master block transactions until MinSynchronousStandbys real standbys are available
if len(synchronousStandbys) < int(*clusterSpec.MinSynchronousStandbys) {
c := len(synchronousStandbys)
for _, es := range clusterSpec.ExternalFallbackSyncStandbys {
externalSynchronousStandbys[es] = struct{}{}
c++
if c == int(*clusterSpec.MinSynchronousStandbys) {
break
}
}
}

if len(synchronousStandbys)+len(externalSynchronousStandbys) < int(*clusterSpec.MinSynchronousStandbys) {
log.Infow("using a fake synchronous standby since there are not enough real standbys available", "masterDB", masterDB.UID, "required", int(*clusterSpec.MinSynchronousStandbys))
addFakeStandby = true
Expand Down
1 change: 1 addition & 0 deletions doc/cluster_spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Some options in a running cluster specification can be changed to update the des
| maxSynchronousStandbys | maximum number of required synchronous standbys when synchronous replication is enabled (only set this to a value > 1 when using PostgreSQL >= 9.6) | no | uint16 | 1 |
| additionalWalSenders | number of additional wal_senders in addition to the ones internally defined by stolon, useful to provide enough wal senders for external standbys (changing this value requires an instance restart) | no | uint16 | 5 |
| additionalMasterReplicationSlots | a list of additional replication slots to be created on the master postgres instance. Replication slots not defined here will be dropped from the master instance (i.e. manually created replication slots will be removed). | no | []string | null |
| externalFallbackSyncStandbys | a list of additional names for possible standbys not included into stolon cluster, but used as replication destinations. Using this attribute you can specify additional PostgreSQL instances that should be used as synchronous standbys. | no | []string | null |
| usePgrewind | try to use pg_rewind for faster instance resyncronization. | no | bool | false |
| initMode | The cluster initialization mode. Can be *new* or *existing*. *new* means that a new db cluster will be created on a random keeper and the other keepers will sync with it. *existing* means that a keeper (that needs to have an already created db cluster) will be choosed as the initial master and the other keepers will sync with it. In this case the `existingConfig` object needs to be populated. | yes | string | |
| existingConfig | configuration for initMode of type "existing" | if initMode is "existing" | ExistingConfig | |
Expand Down
9 changes: 9 additions & 0 deletions doc/syncrepl.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,12 @@ Set MinSynchronousStandbys/MaxSynchronousStandbys to a value different than 1 on
```
stolonctl --cluster-name=mycluster --store-backend=etcd update --patch '{ "synchronousReplication" : true, "minSynchronousStandbys": 2, "maxSynchronousStandbys": 3 }'
```

## Use PostgreSQL instances not included into Stolon cluster as possible synchronous standbys
Sometimes it is necessary to use an external PostgreSQL instance as a synchronous standby, and this instance due to some
reasons is not included into the Stolon cluster. For instance it can be the Barman that is desired to be the synchronous
with the primary PostgreSQL instance which is hosted by the Stolon cluster.
In order to do this specify externalFallbackSyncStandbys attribute.
```
stolonctl --cluster-name=mycluster --store-backend=etcd update --patch --patch '{ "externalFallbackSyncStandbys" : ["my_external_standby"] }' update
```
27 changes: 27 additions & 0 deletions pkg/cluster/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"github.com/mitchellh/copystructure"
"github.com/sorintlab/stolon/common"
util "github.com/sorintlab/stolon/pkg/postgresql"
"regexp"
)

func Uint16P(u uint16) *uint16 {
Expand Down Expand Up @@ -244,6 +245,10 @@ type ClusterSpec struct {
// here will be dropped from the master instance (i.e. manually created
// replication slots will be removed).
AdditionalMasterReplicationSlots []string `json:"additionalMasterReplicationSlots"`
// ExternalFallbackSyncStandbys is used to specify external (not included into stolon cluster)
// postgresql instances which can be used as synchronous standby-s when there are not enough
// synchronous standbys according to the parameter MinSynchronousStandbys
ExternalFallbackSyncStandbys []string `json:"externalFallbackSyncStandbys"`
// Whether to use pg_rewind
UsePgrewind *bool `json:"usePgrewind,omitempty"`
// InitMode defines the cluster initialization mode. Current modes are: new, existing, pitr
Expand Down Expand Up @@ -424,6 +429,11 @@ func (os *ClusterSpec) Validate() error {
if s.InitMode == nil {
return fmt.Errorf("initMode undefined")
}
for _, externalSyncStandbyName := range s.ExternalFallbackSyncStandbys {
if err := validateExternalSyncStandby(externalSyncStandbyName); err != nil {
return err
}
}
for _, replicationSlot := range s.AdditionalMasterReplicationSlots {
if err := validateReplicationSlot(replicationSlot); err != nil {
return err
Expand Down Expand Up @@ -483,6 +493,23 @@ func (os *ClusterSpec) Validate() error {
return nil
}

func validateExternalSyncStandby(externalSyncStandbyName string) error {
correctSymbolsNameFilter := regexp.MustCompile("[_0-9A-Za-z]")

if trimmedStandbyName := strings.TrimSpace(externalSyncStandbyName); len(trimmedStandbyName) == 0 ||
len(correctSymbolsNameFilter.ReplaceAllString(trimmedStandbyName, "")) > 0 ||
strings.Contains(externalSyncStandbyName, "\n") ||
len(trimmedStandbyName) != len(externalSyncStandbyName) {
return fmt.Errorf("external fallback synchronous standby name \"%s\" has an incorrect format, it may only contain letters, numbers, and the underscore character", externalSyncStandbyName)
}

if common.IsStolonName(externalSyncStandbyName) {
return fmt.Errorf("external fallback synchronous standby name \"%s\" has an incorrect format, it should not have prefix 'stolon_'", externalSyncStandbyName)
}

return nil
}

func validateReplicationSlot(replicationSlot string) error {
if !util.IsValidReplSlotName(replicationSlot) {
return fmt.Errorf("wrong replication slot name: %q", replicationSlot)
Expand Down