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
11 changes: 8 additions & 3 deletions internal/management/controller/slots/runner/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,13 +72,18 @@ func (sr *Replicator) Start(ctx context.Context) error {
case <-ticker.C:
}

// If replication is disabled stop the timer,
// the process will resume through the wakeUp channel if necessary
// If replication is disabled, stop the timer and perform cleanup
if config == nil || !config.GetEnabled() {
ticker.Stop()
// we set updateInterval to 0 to make sure the Ticker will be reset
// We set updateInterval to 0 to make sure the Ticker will be reset
// if the feature is enabled again
updateInterval = 0
// Perform cleanup before continuing
if config != nil {
if err := sr.reconcile(ctx, config); err != nil {
contextLog.Warning("cleanup during reconcile when features are disabled", "err", err)
}
}
continue
}

Expand Down
65 changes: 43 additions & 22 deletions internal/webhook/v1/cluster_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -2273,33 +2273,54 @@ func (v *ClusterCustomValidator) validateReplicationSlotsChange(r, old *apiv1.Cl
newReplicationSlots := r.Spec.ReplicationSlots
oldReplicationSlots := old.Spec.ReplicationSlots

if oldReplicationSlots == nil || oldReplicationSlots.HighAvailability == nil ||
!oldReplicationSlots.HighAvailability.GetEnabled() {
var errs field.ErrorList

if oldReplicationSlots == nil {
return nil
}

var errs field.ErrorList
// Validate HighAvailability changes
if oldReplicationSlots.HighAvailability.GetEnabled() {
// When disabling, we should check that the prefix is not removed and doesn't change
// to properly execute the cleanup logic
if newReplicationSlots == nil || newReplicationSlots.HighAvailability == nil {
path := field.NewPath("spec", "replicationSlots")
if newReplicationSlots != nil {
path = path.Child("highAvailability")
}
errs = append(errs,
field.Invalid(
path,
nil,
fmt.Sprintf("Cannot remove %v section while highAvailability is enabled", path)),
)
} else if oldReplicationSlots.HighAvailability.SlotPrefix != newReplicationSlots.HighAvailability.SlotPrefix {
errs = append(errs,
field.Invalid(
field.NewPath("spec", "replicationSlots", "highAvailability", "slotPrefix"),
newReplicationSlots.HighAvailability.SlotPrefix,
"Cannot change replication slot prefix while highAvailability is enabled"),
)
}
}

// when disabling we should check that the prefix it's not removed, and it doesn't change to
// properly execute the cleanup logic
if newReplicationSlots == nil || newReplicationSlots.HighAvailability == nil {
path := field.NewPath("spec", "replicationSlots")
if newReplicationSlots != nil {
path = path.Child("highAvailability")
// Validate SynchronizeReplicas changes
// When synchronizeReplicas is enabled, we need to ensure users disable it before removing the configuration
// to allow the cleanup logic to properly remove user-defined replication slots from replicas
if oldReplicationSlots.SynchronizeReplicas.GetEnabled() {
if newReplicationSlots == nil || newReplicationSlots.SynchronizeReplicas == nil {
path := field.NewPath("spec", "replicationSlots")
if newReplicationSlots != nil {
path = path.Child("synchronizeReplicas")
}
errs = append(errs,
field.Invalid(
path,
nil,
fmt.Sprintf("Cannot remove %v section while synchronizeReplicas is enabled. "+
"Disable synchronizeReplicas first to allow cleanup of user-defined replication slots on replicas", path)),
)
}
errs = append(errs,
field.Invalid(
path,
nil,
fmt.Sprintf("Cannot remove %v section while highAvailability is enabled", path)),
)
} else if oldReplicationSlots.HighAvailability.SlotPrefix != newReplicationSlots.HighAvailability.SlotPrefix {
errs = append(errs,
field.Invalid(
field.NewPath("spec", "replicationSlots", "highAvailability", "slotPrefix"),
newReplicationSlots.HighAvailability.SlotPrefix,
"Cannot change replication slot prefix while highAvailability is enabled"),
)
}

return errs
Expand Down
120 changes: 119 additions & 1 deletion internal/webhook/v1/cluster_webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3546,6 +3546,9 @@ var _ = Describe("validation of replication slots configuration", func() {
HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
Enabled: ptr.To(false),
},
SynchronizeReplicas: &apiv1.SynchronizeReplicasConfiguration{
Enabled: ptr.To(false),
},
},
},
}
Expand All @@ -3557,6 +3560,9 @@ var _ = Describe("validation of replication slots configuration", func() {
Enabled: ptr.To(true),
SlotPrefix: "_test_",
},
SynchronizeReplicas: &apiv1.SynchronizeReplicasConfiguration{
Enabled: ptr.To(true),
},
}

Expect(v.validateReplicationSlotsChange(newCluster, oldCluster)).To(BeEmpty())
Expand All @@ -3581,7 +3587,7 @@ var _ = Describe("validation of replication slots configuration", func() {
Expect(v.validateReplicationSlotsChange(newCluster, oldCluster)).To(HaveLen(1))
})

It("prevents removing the replication slot section when replication slots are enabled", func() {
It("prevents removing the replication slot section when highAvailability is enabled", func() {
oldCluster := &apiv1.Cluster{
Spec: apiv1.ClusterSpec{
ImageName: versions.DefaultImageName,
Expand All @@ -3590,6 +3596,30 @@ var _ = Describe("validation of replication slots configuration", func() {
Enabled: ptr.To(true),
SlotPrefix: "_test_",
},
SynchronizeReplicas: &apiv1.SynchronizeReplicasConfiguration{
Enabled: ptr.To(false),
},
},
},
}
oldCluster.Default()

newCluster := oldCluster.DeepCopy()
newCluster.Spec.ReplicationSlots = nil
Expect(v.validateReplicationSlotsChange(newCluster, oldCluster)).To(HaveLen(1))
})

It("prevents removing the replication slot section when synchronizeReplicas is enabled", func() {
oldCluster := &apiv1.Cluster{
Spec: apiv1.ClusterSpec{
ImageName: versions.DefaultImageName,
ReplicationSlots: &apiv1.ReplicationSlotsConfiguration{
HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
Enabled: ptr.To(false),
},
SynchronizeReplicas: &apiv1.SynchronizeReplicasConfiguration{
Enabled: ptr.To(true),
},
},
},
}
Expand All @@ -3600,6 +3630,28 @@ var _ = Describe("validation of replication slots configuration", func() {
Expect(v.validateReplicationSlotsChange(newCluster, oldCluster)).To(HaveLen(1))
})

It("prevents removing the replication slot section when both features are enabled", func() {
oldCluster := &apiv1.Cluster{
Spec: apiv1.ClusterSpec{
ImageName: versions.DefaultImageName,
ReplicationSlots: &apiv1.ReplicationSlotsConfiguration{
HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
Enabled: ptr.To(true),
SlotPrefix: "_test_",
},
SynchronizeReplicas: &apiv1.SynchronizeReplicasConfiguration{
Enabled: ptr.To(true),
},
},
},
}
oldCluster.Default()

newCluster := oldCluster.DeepCopy()
newCluster.Spec.ReplicationSlots = nil
Expect(v.validateReplicationSlotsChange(newCluster, oldCluster)).To(HaveLen(2))
})

It("allows disabling the replication slots", func() {
oldCluster := &apiv1.Cluster{
Spec: apiv1.ClusterSpec{
Expand Down Expand Up @@ -3663,6 +3715,72 @@ var _ = Describe("validation of replication slots configuration", func() {
Expect(errors).To(BeEmpty())
})

It("prevents removing the synchronizeReplicas section when synchronizeReplicas is enabled", func() {
oldCluster := &apiv1.Cluster{
Spec: apiv1.ClusterSpec{
ImageName: versions.DefaultImageName,
ReplicationSlots: &apiv1.ReplicationSlotsConfiguration{
HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
Enabled: ptr.To(false),
},
SynchronizeReplicas: &apiv1.SynchronizeReplicasConfiguration{
Enabled: ptr.To(true),
},
},
},
}
oldCluster.Default()

newCluster := oldCluster.DeepCopy()
newCluster.Spec.ReplicationSlots.SynchronizeReplicas = nil
errs := v.validateReplicationSlotsChange(newCluster, oldCluster)
Expect(errs).To(HaveLen(1))
Expect(errs[0].Error()).To(ContainSubstring("Cannot remove"))
Expect(errs[0].Error()).To(ContainSubstring("synchronizeReplicas"))
})

It("allows disabling the synchronizeReplicas feature", func() {
oldCluster := &apiv1.Cluster{
Spec: apiv1.ClusterSpec{
ImageName: versions.DefaultImageName,
ReplicationSlots: &apiv1.ReplicationSlotsConfiguration{
HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
Enabled: ptr.To(false),
},
SynchronizeReplicas: &apiv1.SynchronizeReplicasConfiguration{
Enabled: ptr.To(true),
},
},
},
}
oldCluster.Default()

newCluster := oldCluster.DeepCopy()
newCluster.Spec.ReplicationSlots.SynchronizeReplicas.Enabled = ptr.To(false)
Expect(v.validateReplicationSlotsChange(newCluster, oldCluster)).To(BeEmpty())
})

It("allows removing synchronizeReplicas section when synchronizeReplicas is disabled", func() {
oldCluster := &apiv1.Cluster{
Comment thread
mnencia marked this conversation as resolved.
Resolved
Spec: apiv1.ClusterSpec{
ImageName: versions.DefaultImageName,
ReplicationSlots: &apiv1.ReplicationSlotsConfiguration{
HighAvailability: &apiv1.ReplicationSlotsHAConfiguration{
Enabled: ptr.To(false),
},
SynchronizeReplicas: &apiv1.SynchronizeReplicasConfiguration{
Enabled: ptr.To(false),
},
},
},
}
oldCluster.Default()

newCluster := oldCluster.DeepCopy()
newCluster.Spec.ReplicationSlots.SynchronizeReplicas = nil
Expect(v.validateReplicationSlotsChange(newCluster, oldCluster)).To(BeEmpty())
})

It("returns no errors when synchronizeLogicalDecoding is disabled", func() {
cluster := &apiv1.Cluster{
Spec: apiv1.ClusterSpec{
Expand Down
Loading