Skip to content

Commit 8d75f56

Browse files
committed
SCF-830: Steamlined cluster update function
1 parent 19ca281 commit 8d75f56

2 files changed

Lines changed: 116 additions & 80 deletions

File tree

pkg/cluster/cluster.go

Lines changed: 113 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1009,91 +1009,28 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
10091009
defer c.mu.Unlock()
10101010

10111011
// Block all spec changes when cluster is stopped or stopping
1012-
if c.Status.Stopped() || c.Status.Stopping() {
1013-
lifecyclePhase := ""
1014-
if newSpec.Spec.Lifecycle != nil {
1015-
lifecyclePhase = newSpec.Spec.Lifecycle.Phase
1016-
}
1017-
// During Stopping: block ALL spec changes (no cancellation allowed)
1018-
// During Stopped: only block if keeping lifecycle.phase="stopped"
1019-
if c.Status.Stopping() {
1020-
return fmt.Errorf("cannot update cluster while it is stopping. Wait for it to fully stop first")
1021-
}
1022-
if lifecyclePhase == "stopped" {
1023-
return fmt.Errorf("cannot update cluster while stopped. Remove lifecycle.phase to wake up the cluster")
1024-
}
1012+
blocked, err := c.blockLifecycleUpdate(newSpec)
1013+
if err != nil {
1014+
return err
1015+
}
1016+
if blocked {
1017+
return nil
10251018
}
10261019

10271020
newSpec.Status.PostgresClusterStatus = acidv1.ClusterStatusUpdating
10281021

1029-
newSpec, err := c.KubeClient.SetPostgresCRDStatus(c.clusterName(), newSpec)
1022+
newSpec, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), newSpec)
10301023
if err != nil {
10311024
return fmt.Errorf("could not set cluster status to updating: %w", err)
10321025
}
10331026

1034-
// Check if user is initiating hibernate (Running -> Stopping)
1035-
if c.Status.Running() && newSpec.Spec.Lifecycle != nil && newSpec.Spec.Lifecycle.Phase == "stopped" {
1036-
c.logger.Infof("[lifecycle] initiating hibernate for cluster %s: current numberOfInstances=%d", c.Name, c.Spec.NumberOfInstances)
1037-
1038-
// Store previousNumberOfInstances BEFORE setting numberOfInstances to 0
1039-
newSpec.Status.PreviousNumberOfInstances = c.Spec.NumberOfInstances
1040-
newSpec.Spec.NumberOfInstances = 0
1041-
newSpec.Status.PostgresClusterStatus = acidv1.ClusterStatusStopping
1042-
1043-
c.logger.Infof("[lifecycle] hibernate initiated: setting numberOfInstances=0, previousNumberOfInstances=%d", newSpec.Status.PreviousNumberOfInstances)
1044-
1045-
// Update spec first (Update only updates spec when CR has status subresource)
1046-
pgUpdated, err := c.KubeClient.UpdatePostgresCR(c.clusterName(), newSpec)
1047-
if err != nil {
1048-
return fmt.Errorf("could not update spec during hibernate: %w", err)
1049-
}
1050-
c.logger.Infof("[lifecycle] hibernate: spec updated successfully")
1051-
1052-
// Update status separately - we need to preserve the status values we set
1053-
// because UpdatePostgresCR returns object with status zeroed (subresource behavior)
1054-
pgUpdated.Status.PreviousNumberOfInstances = newSpec.Status.PreviousNumberOfInstances
1055-
pgUpdated.Status.PostgresClusterStatus = newSpec.Status.PostgresClusterStatus
1056-
1057-
pgUpdated, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), pgUpdated)
1058-
if err != nil {
1059-
return fmt.Errorf("could not update status during hibernate: %w", err)
1060-
}
1061-
c.logger.Infof("[lifecycle] hibernate: status updated successfully, previousNumberOfInstances=%d", pgUpdated.Status.PreviousNumberOfInstances)
1062-
1063-
c.setSpec(pgUpdated)
1064-
return nil
1027+
// Handle lifecycle transitions (hibernate/wake-up)
1028+
handled, err := c.handleHibernateAndWakeUp(newSpec)
1029+
if err != nil {
1030+
return err
10651031
}
1066-
1067-
// Check if user is waking up from stopped state (Stopped -> Running)
1068-
// This is when user clears lifecycle.phase to wake up the cluster
1069-
if c.Status.Stopped() && (newSpec.Spec.Lifecycle == nil || newSpec.Spec.Lifecycle.Phase != "stopped") {
1070-
if newSpec.Status.PreviousNumberOfInstances > 0 {
1071-
c.logger.Infof("[lifecycle] waking up cluster %s: restoring numberOfInstances=%d", c.Name, newSpec.Status.PreviousNumberOfInstances)
1072-
1073-
// Restore numberOfInstances from previousNumberOfInstances
1074-
newSpec.Spec.NumberOfInstances = newSpec.Status.PreviousNumberOfInstances
1075-
newSpec.Status.PostgresClusterStatus = acidv1.ClusterStatusUpdating
1076-
1077-
// Update spec first
1078-
pgUpdated, err := c.KubeClient.UpdatePostgresCR(c.clusterName(), newSpec)
1079-
if err != nil {
1080-
return fmt.Errorf("could not update spec during wake-up: %w", err)
1081-
}
1082-
c.logger.Infof("[lifecycle] wake-up: spec updated successfully")
1083-
1084-
// Update status separately, and clear previousNumberOfInstances after restore
1085-
pgUpdated.Status.PreviousNumberOfInstances = 0 // Clear after successful restore
1086-
pgUpdated.Status.PostgresClusterStatus = newSpec.Status.PostgresClusterStatus
1087-
1088-
pgUpdated, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), pgUpdated)
1089-
if err != nil {
1090-
return fmt.Errorf("could not update status during wake-up: %w", err)
1091-
}
1092-
c.logger.Infof("[lifecycle] wake-up: status updated successfully, previousNumberOfInstances cleared")
1093-
1094-
c.setSpec(pgUpdated)
1095-
return nil
1096-
}
1032+
if handled {
1033+
return nil
10971034
}
10981035

10991036
if !c.isInMaintenanceWindow(newSpec.Spec.MaintenanceWindows) {
@@ -1301,6 +1238,106 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
13011238
return nil
13021239
}
13031240

1241+
// blockLifecycleUpdate checks if an update should be blocked due to lifecycle state.
1242+
// Returns (blocked bool, err error):
1243+
// - (true, nil) if update is blocked and caller should return early
1244+
// - (false, nil) if update can proceed
1245+
// - (false, error) on error
1246+
func (c *Cluster) blockLifecycleUpdate(newSpec *acidv1.Postgresql) (bool, error) {
1247+
if !c.Status.Stopped() && !c.Status.Stopping() {
1248+
return false, nil
1249+
}
1250+
1251+
lifecyclePhase := ""
1252+
if newSpec.Spec.Lifecycle != nil {
1253+
lifecyclePhase = newSpec.Spec.Lifecycle.Phase
1254+
}
1255+
1256+
// During Stopping: block ALL spec changes (no cancellation allowed)
1257+
if c.Status.Stopping() {
1258+
return true, fmt.Errorf("cannot update cluster while it is stopping. Wait for it to fully stop first")
1259+
}
1260+
1261+
// During Stopped: only block if keeping lifecycle.phase="stopped"
1262+
if lifecyclePhase == "stopped" {
1263+
return true, fmt.Errorf("cannot update cluster while stopped. Remove lifecycle.phase to wake up the cluster")
1264+
}
1265+
1266+
return false, nil
1267+
}
1268+
1269+
// handleHibernateAndWakeUp processes lifecycle hibernate/wake-up transitions.
1270+
// Returns (handled bool, err error):
1271+
// - (true, nil) if lifecycle transition was handled, Update() should return early
1272+
// - (false, nil) if no lifecycle transition, normal update continues
1273+
// - (false, error) on error
1274+
func (c *Cluster) handleHibernateAndWakeUp(newSpec *acidv1.Postgresql) (bool, error) {
1275+
// === INITIATE HIBERNATE: Running -> Stopping ===
1276+
if c.Status.Running() && newSpec.Spec.Lifecycle != nil && newSpec.Spec.Lifecycle.Phase == "stopped" {
1277+
c.logger.Infof("[lifecycle] initiating hibernate for cluster %s: current numberOfInstances=%d", c.Name, c.Spec.NumberOfInstances)
1278+
1279+
// Store previousNumberOfInstances BEFORE setting numberOfInstances to 0
1280+
newSpec.Status.PreviousNumberOfInstances = c.Spec.NumberOfInstances
1281+
newSpec.Spec.NumberOfInstances = 0
1282+
newSpec.Status.PostgresClusterStatus = acidv1.ClusterStatusStopping
1283+
1284+
c.logger.Infof("[lifecycle] hibernate initiated: setting numberOfInstances=0, previousNumberOfInstances=%d", newSpec.Status.PreviousNumberOfInstances)
1285+
1286+
// Update spec first (Update only updates spec when CR has status subresource)
1287+
pgUpdated, err := c.KubeClient.UpdatePostgresCR(c.clusterName(), newSpec)
1288+
if err != nil {
1289+
return false, fmt.Errorf("could not update spec during hibernate: %w", err)
1290+
}
1291+
c.logger.Infof("[lifecycle] hibernate: spec updated successfully")
1292+
1293+
// Update status separately - preserve status values since UpdatePostgresCR returns object with status zeroed
1294+
pgUpdated.Status.PreviousNumberOfInstances = newSpec.Status.PreviousNumberOfInstances
1295+
pgUpdated.Status.PostgresClusterStatus = newSpec.Status.PostgresClusterStatus
1296+
1297+
pgUpdated, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), pgUpdated)
1298+
if err != nil {
1299+
return false, fmt.Errorf("could not update status during hibernate: %w", err)
1300+
}
1301+
c.logger.Infof("[lifecycle] hibernate: status updated successfully, previousNumberOfInstances=%d", pgUpdated.Status.PreviousNumberOfInstances)
1302+
1303+
c.setSpec(pgUpdated)
1304+
return true, nil
1305+
}
1306+
1307+
// === WAKE-UP: Stopped -> Running ===
1308+
if c.Status.Stopped() && (newSpec.Spec.Lifecycle == nil || newSpec.Spec.Lifecycle.Phase != "stopped") {
1309+
if newSpec.Status.PreviousNumberOfInstances > 0 {
1310+
c.logger.Infof("[lifecycle] waking up cluster %s: restoring numberOfInstances=%d", c.Name, newSpec.Status.PreviousNumberOfInstances)
1311+
1312+
// Restore numberOfInstances from previousNumberOfInstances
1313+
newSpec.Spec.NumberOfInstances = newSpec.Status.PreviousNumberOfInstances
1314+
newSpec.Status.PostgresClusterStatus = acidv1.ClusterStatusUpdating
1315+
1316+
// Update spec first
1317+
pgUpdated, err := c.KubeClient.UpdatePostgresCR(c.clusterName(), newSpec)
1318+
if err != nil {
1319+
return false, fmt.Errorf("could not update spec during wake-up: %w", err)
1320+
}
1321+
c.logger.Infof("[lifecycle] wake-up: spec updated successfully")
1322+
1323+
// Update status separately, and clear previousNumberOfInstances after restore
1324+
pgUpdated.Status.PreviousNumberOfInstances = 0
1325+
pgUpdated.Status.PostgresClusterStatus = newSpec.Status.PostgresClusterStatus
1326+
1327+
pgUpdated, err = c.KubeClient.SetPostgresCRDStatus(c.clusterName(), pgUpdated)
1328+
if err != nil {
1329+
return false, fmt.Errorf("could not update status during wake-up: %w", err)
1330+
}
1331+
c.logger.Infof("[lifecycle] wake-up: status updated successfully, previousNumberOfInstances cleared")
1332+
1333+
c.setSpec(pgUpdated)
1334+
return true, nil
1335+
}
1336+
}
1337+
1338+
return false, nil
1339+
}
1340+
13041341
func syncResources(a, b *v1.ResourceRequirements) bool {
13051342
for _, res := range []v1.ResourceName{
13061343
v1.ResourceCPU,

pkg/cluster/lifecycle.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,21 +13,19 @@ import (
1313
// - Stopped -> Updating: When user clears lifecycle.phase (wake-up)
1414
// - Updating -> Running: Normal sync continues, defer sets final status
1515
func (c *Cluster) manageHibernateState(oldSpec acidv1.Postgresql, newSpec *acidv1.Postgresql) bool {
16-
17-
// FIX B: Detect wake-up by comparing oldSpec status vs newSpec status
1816
// When Update() is called, it sets status=Updating before Sync() runs.
1917
// So we need to check if oldSpec.Status was Stopped and newSpec is Updating
2018
// with lifecycle cleared to properly detect wake-up.
2119
isWakingUp := oldSpec.Status.Stopped() &&
2220
newSpec.Status.PostgresClusterStatus == acidv1.ClusterStatusUpdating &&
2321
(newSpec.Spec.Lifecycle == nil || newSpec.Spec.Lifecycle.Phase != "stopped")
2422

25-
// FIX C: Additional wake-up detection with simpler condition
2623
// If lifecycle was cleared and we have previousNumberOfInstances and numberOfInstances is 0
2724
isWakingUpSimple := newSpec.Spec.Lifecycle == nil || newSpec.Spec.Lifecycle.Phase != "stopped"
2825
hasPreviousInstances := newSpec.Status.PreviousNumberOfInstances > 0
2926
needsRestore := newSpec.Spec.NumberOfInstances == 0
3027

28+
// double verification of waking up
3129
isWakingUp = isWakingUp || (isWakingUpSimple && hasPreviousInstances && needsRestore)
3230

3331
// === INITIATE HIBERNATE: Running -> Stopping ===
@@ -75,4 +73,5 @@ func (c *Cluster) manageHibernateState(oldSpec acidv1.Postgresql, newSpec *acidv
7573
}
7674

7775
return true
78-
}
76+
}
77+

0 commit comments

Comments
 (0)