-
Notifications
You must be signed in to change notification settings - Fork 180
feat(flow): rack-scale decommission workflow with proto mirror sync a… #5063
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -647,10 +647,24 @@ func executeDecommissionControlAction(actx actionExecutionContext) error { | |||||||||||||
| ).Get(ctx, nil) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // maxConsecutiveFailureDuration is the time span over which consecutive | ||||||||||||||
| // GetDecommissionStatus errors must occur before the wait loop aborts. | ||||||||||||||
| // A time-based budget scales with the configured poll interval rather than | ||||||||||||||
| // being coupled to a fixed attempt count: a Core outage that outlasts this | ||||||||||||||
| // window is treated as unrecoverable and the workflow returns an error. | ||||||||||||||
| const maxConsecutiveFailureDuration = 5 * time.Minute | ||||||||||||||
|
|
||||||||||||||
| // executeWaitDecommissionedAction polls GetDecommissionStatus until all | ||||||||||||||
| // components reach the "Decommissioned" terminal state. States that begin | ||||||||||||||
| // with "Decommissioning/" are in-progress; any other non-terminal state is | ||||||||||||||
| // treated as an error. Uses config.Timeout and config.PollInterval. | ||||||||||||||
| // with "Decommissioning/" are in-progress; any other state is a hard failure. | ||||||||||||||
| // | ||||||||||||||
| // A component absent from Core's response (state "") is treated as already | ||||||||||||||
| // decommissioned: Core removes the resource record as the final step, so an | ||||||||||||||
| // absent ID is the expected terminal condition rather than an error. | ||||||||||||||
| // | ||||||||||||||
| // Consecutive GetDecommissionStatus failures are counted; after | ||||||||||||||
| // maxConsecutiveStatusFailures the loop aborts rather than spinning until the | ||||||||||||||
| // deadline. Uses config.Timeout and config.PollInterval. | ||||||||||||||
|
Comment on lines
+665
to
+667
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win Update the stale failure-budget comment. The code uses Proposed fix-// Consecutive GetDecommissionStatus failures are counted; after
-// maxConsecutiveStatusFailures the loop aborts rather than spinning until the
-// deadline. Uses config.Timeout and config.PollInterval.
+// A consecutive GetDecommissionStatus failure period that reaches
+// maxConsecutiveFailureDuration aborts the loop rather than spinning until the
+// deadline. Uses config.Timeout and config.PollInterval.📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| func executeWaitDecommissionedAction(actx actionExecutionContext) error { | ||||||||||||||
| ctx := actx.workflowContext | ||||||||||||||
| target := actx.target | ||||||||||||||
|
|
@@ -671,6 +685,7 @@ func executeWaitDecommissionedAction(actx actionExecutionContext) error { | |||||||||||||
| Msg("Waiting for decommission to complete") | ||||||||||||||
|
|
||||||||||||||
| deadline := workflow.Now(ctx).Add(timeout) | ||||||||||||||
| var firstFailureAt time.Time | ||||||||||||||
|
|
||||||||||||||
| for { | ||||||||||||||
| if workflow.Now(ctx).After(deadline) { | ||||||||||||||
|
|
@@ -683,38 +698,81 @@ func executeWaitDecommissionedAction(actx actionExecutionContext) error { | |||||||||||||
| return fmt.Errorf("workflow sleep interrupted: %w", err) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Use a short fire-once policy so a hung status call fails quickly | ||||||||||||||
| // and the poll loop's time-based failure budget controls retries. | ||||||||||||||
| statusCtx := workflow.WithActivityOptions(ctx, workflow.ActivityOptions{ | ||||||||||||||
| ScheduleToCloseTimeout: 30 * time.Second, | ||||||||||||||
| StartToCloseTimeout: 30 * time.Second, | ||||||||||||||
| RetryPolicy: &temporal.RetryPolicy{ | ||||||||||||||
| MaximumAttempts: 1, | ||||||||||||||
| }, | ||||||||||||||
| }) | ||||||||||||||
| var result activity.GetDecommissionStatusResult | ||||||||||||||
| err := workflow.ExecuteActivity( | ||||||||||||||
| ctx, activity.NameGetDecommissionStatus, target, | ||||||||||||||
| ).Get(ctx, &result) | ||||||||||||||
| statusCtx, activity.NameGetDecommissionStatus, target, | ||||||||||||||
| ).Get(statusCtx, &result) | ||||||||||||||
| if err != nil { | ||||||||||||||
| log.Warn().Err(err).Msg("Failed to get decommission status, will retry") | ||||||||||||||
| now := workflow.Now(ctx) | ||||||||||||||
| if firstFailureAt.IsZero() { | ||||||||||||||
| firstFailureAt = now | ||||||||||||||
| } | ||||||||||||||
| elapsed := now.Sub(firstFailureAt) | ||||||||||||||
| log.Warn(). | ||||||||||||||
| Err(err). | ||||||||||||||
| Dur("consecutive_failure_duration", elapsed). | ||||||||||||||
| Dur("limit", maxConsecutiveFailureDuration). | ||||||||||||||
| Msg("Failed to get decommission status") | ||||||||||||||
| if elapsed >= maxConsecutiveFailureDuration { | ||||||||||||||
| return fmt.Errorf( | ||||||||||||||
| "aborting: GetDecommissionStatus has been failing for %v: %w", | ||||||||||||||
| elapsed, err, | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
| continue | ||||||||||||||
| } | ||||||||||||||
| firstFailureAt = time.Time{} // reset on success | ||||||||||||||
|
|
||||||||||||||
| // Treat the result as complete only if Core reported at least one | ||||||||||||||
| // component (found or absent). An entirely empty response — no states | ||||||||||||||
| // and no not-found IDs — means the query produced no output and we | ||||||||||||||
| // should keep polling rather than declaring success. | ||||||||||||||
| allDecommissioned := len(result.States)+len(result.NotFound) > 0 | ||||||||||||||
|
|
||||||||||||||
| allDecommissioned := true | ||||||||||||||
| for componentID, state := range result.States { | ||||||||||||||
| if state == "Decommissioned" { | ||||||||||||||
| continue | ||||||||||||||
| } | ||||||||||||||
| if strings.HasPrefix(state, "Decommissioning/") { | ||||||||||||||
| switch { | ||||||||||||||
| case state == "Decommissioned": | ||||||||||||||
| // Terminal success. | ||||||||||||||
| case strings.HasPrefix(state, "Decommissioning/"): | ||||||||||||||
| allDecommissioned = false | ||||||||||||||
| log.Debug(). | ||||||||||||||
| Str("component_id", componentID). | ||||||||||||||
| Str("state", state). | ||||||||||||||
| Msg("Component still decommissioning") | ||||||||||||||
| continue | ||||||||||||||
| default: | ||||||||||||||
| return fmt.Errorf( | ||||||||||||||
| "decommission failed for component %s: reached unexpected state %q", | ||||||||||||||
| componentID, state, | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
| // Any other state is unexpected/terminal-error | ||||||||||||||
| return fmt.Errorf( | ||||||||||||||
| "decommission failed for component %s: unexpected state %q", | ||||||||||||||
| componentID, state, | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // NotFound components are logged at Warn and treated as terminal. | ||||||||||||||
| // Core removes the resource record as its final decommission step, so | ||||||||||||||
| // absence is the expected success signal. However, absence can also mean | ||||||||||||||
| // an unknown or mistyped ID; without an explicit Core API signal we | ||||||||||||||
| // cannot tell the two apart. | ||||||||||||||
| // TODO: add a definitive "Decommissioned" state or found/not-found flag | ||||||||||||||
| // to the Core API so this ambiguity can be resolved. | ||||||||||||||
| for _, id := range result.NotFound { | ||||||||||||||
| log.Warn(). | ||||||||||||||
| Str("component_id", id). | ||||||||||||||
| Msg("Component absent from Core during decommission poll; treating as terminal (record removed or unknown ID)") | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| if allDecommissioned { | ||||||||||||||
| log.Info(). | ||||||||||||||
| Int("count", len(result.States)). | ||||||||||||||
| Int("states_count", len(result.States)). | ||||||||||||||
| Int("not_found_count", len(result.NotFound)). | ||||||||||||||
| Msg("All components decommissioned") | ||||||||||||||
| return nil | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Do not convert an empty state into terminal success.
Lines 245-250 classify every empty raw state as
NotFound. The result contract states that this can also mean an unknown or mistyped ID. The workflow then treats that value as terminal success.Return explicit record-presence and terminal-state information from Core. Only a confirmed terminal decommission state must complete the workflow.
🤖 Prompt for AI Agents