Repository navigation
fix(operator): keep a package's own interrupt after a config change #878
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
Changes from 1 commit
ef38a35
4e07bb3
510a3fe
36df6a7
77b303f
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 |
|---|---|---|
|
|
@@ -548,8 +548,16 @@ type NodeWrightStatus struct { | |
|
|
||
| type NodeState map[string]PackageStatus | ||
|
|
||
| // Upsert adds or updates specified state for package in the node state | ||
| // Upsert adds or updates specified state for package in the node state, carrying the entry's | ||
| // InterruptPending flag through unchanged apart from clearing it at post-interrupt. | ||
| func (ns *NodeState) Upsert(_package PackageRef, image string, state State, stage Stage, restarts int32, containerSHA string) bool { | ||
| return ns.UpsertOwing(_package, image, state, stage, restarts, containerSHA, false) | ||
| } | ||
|
|
||
| // UpsertOwing is Upsert for a package that declares its own interrupt when ownInterrupt is set: | ||
| // an entry written at apply or upgrade then owes that interrupt until the package reaches | ||
| // post-interrupt. | ||
| func (ns *NodeState) UpsertOwing(_package PackageRef, image string, state State, stage Stage, restarts int32, containerSHA string, ownInterrupt bool) bool { | ||
|
|
||
| if *ns == nil { | ||
| *ns = make(map[string]PackageStatus) | ||
|
|
@@ -566,6 +574,15 @@ func (ns *NodeState) Upsert(_package PackageRef, image string, state State, stag | |
| } | ||
|
|
||
| existing, ok := (*ns)[_package.GetUniqueName()] | ||
|
|
||
| status.InterruptPending = ok && existing.InterruptPending | ||
| switch { | ||
| case ownInterrupt && (stage == StageApply || stage == StageUpgrade): | ||
| status.InterruptPending = true | ||
| case stage == StagePostInterrupt: | ||
|
Collaborator
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. issue: The flag is cleared only at Suggestion: also clear the flag at
Contributor
Author
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. Agreed, that would be a second reboot on a node that just rebooted. Fixed in 36df6a7: the flag is now cleared at (interrupt, complete) in |
||
| status.InterruptPending = false | ||
| } | ||
|
|
||
| if ok && status.Equal(&existing) { | ||
| return false | ||
| } | ||
|
|
@@ -575,6 +592,16 @@ func (ns *NodeState) Upsert(_package PackageRef, image string, state State, stag | |
| return true | ||
| } | ||
|
|
||
| // OwesOwnInterrupt reports whether the package declares an interrupt that has not yet run on | ||
| // this node: the entry is missing or still pending. | ||
| func (ns *NodeState) OwesOwnInterrupt(_package Package) bool { | ||
| if !_package.HasInterrupt() { | ||
| return false | ||
| } | ||
| status, ok := (*ns)[_package.GetUniqueName()] | ||
| return !ok || status.InterruptPending | ||
| } | ||
|
|
||
| // RemoveState removes specified package from Node State | ||
| func (ns *NodeState) RemoveState(_package PackageRef) bool { | ||
| if *ns == nil { | ||
|
|
@@ -653,17 +680,15 @@ func (ns *NodeState) IsComplete(packages Packages, interrupt map[string][]*Inter | |
| return false | ||
| } | ||
|
|
||
| // HasInterrupt determines whether package has an interrupt based off of config updates and interrupts passed to it | ||
| // HasInterrupt determines whether package has an interrupt based off of config updates and interrupts passed to it. | ||
| // With config updates recorded, the interrupts matching the changed keys apply, plus the package's own | ||
| // while this node still owes it, so a config change cannot finish a package that never ran its interrupt. | ||
| func (ns *NodeState) HasInterrupt(_package Package, interrupt map[string][]*Interrupt, config map[string][]string) bool { | ||
| var hasInterrupt bool | ||
|
|
||
| if len(config[_package.Name]) > 0 { | ||
| hasInterrupt = len(interrupt[_package.Name]) > 0 | ||
| } else { | ||
| hasInterrupt = _package.HasInterrupt() | ||
| return len(interrupt[_package.Name]) > 0 || ns.OwesOwnInterrupt(_package) | ||
| } | ||
|
|
||
| return hasInterrupt | ||
| return _package.HasInterrupt() | ||
| } | ||
|
|
||
| func (ns *NodeState) NextStage(_package *Package, interrupt map[string][]*Interrupt, config map[string][]string) *Stage { | ||
|
|
@@ -825,17 +850,25 @@ type PackageStatus struct { | |
|
|
||
| // Restarts are the number of times a package restarted | ||
| Restarts int32 `json:"restarts,omitempty"` | ||
|
|
||
| // InterruptPending is set at apply or upgrade for a package that declares its own interrupt, | ||
| // and cleared once the package reaches post-interrupt. While set, that interrupt is still owed on this | ||
| // node, so a config change that matches no configInterrupts does not let it finish without it. | ||
| // Entries written before this field existed read as not pending. | ||
| //+optional | ||
| InterruptPending bool `json:"interruptPending,omitempty"` | ||
| } | ||
|
|
||
| // Equal checks name, version, image, containerSHA, stage, state, and restarts | ||
| // Equal checks name, version, image, containerSHA, stage, state, restarts, and interruptPending | ||
| func (left *PackageStatus) Equal(right *PackageStatus) bool { | ||
| return left.Name == right.Name && | ||
| left.Version == right.Version && | ||
| left.Image == right.Image && | ||
| left.ContainerSHA == right.ContainerSHA && | ||
| left.Stage == right.Stage && | ||
| left.State == right.State && | ||
| left.Restarts == right.Restarts | ||
| left.Restarts == right.Restarts && | ||
| left.InterruptPending == right.InterruptPending | ||
| } | ||
|
|
||
| // IsInterruptStage reports whether the package is in one of the two interrupt-phase | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1658,7 +1658,7 @@ func (r *SkyhookReconciler) RunSkyhookPackages(ctx context.Context, clusterState | |
| return nil, fmt.Errorf("node %s: selecting packages to run: %w", node.GetNode().Name, err) | ||
| } | ||
|
|
||
| interrupt, pack := fudgeInterruptWithPriority(toRun, skyhook.GetSkyhook().GetConfigUpdates(), skyhook.GetSkyhook().GetConfigInterrupts()) | ||
| interrupt, pack := fudgeInterruptWithPriority(toRun, skyhook.GetSkyhook().GetConfigUpdates(), skyhook.GetSkyhook().GetConfigInterrupts(), node.OwesOwnInterrupt) | ||
|
Collaborator
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. issue (non-blocking): No test reaches the controller passing Replacing Add one spec that runs a real node through
Contributor
Author
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. Fair. 36df6a7 pulls the per-node pick into |
||
|
|
||
| for _, f := range toRun { | ||
|
|
||
|
|
@@ -3115,7 +3115,7 @@ func (r *SkyhookReconciler) Interrupt(ctx context.Context, skyhookNode wrapper.S | |
| } | ||
|
|
||
| // fudgeInterruptWithPriority takes a list of packages, interrupts, and configUpdates and returns the correct merged interrupt to run to handle all the packages | ||
| func fudgeInterruptWithPriority(next []*v1alpha1.Package, configUpdates map[string][]string, interrupts map[string][]*v1alpha1.Interrupt) (*v1alpha1.Interrupt, string) { | ||
| func fudgeInterruptWithPriority(next []*v1alpha1.Package, configUpdates map[string][]string, interrupts map[string][]*v1alpha1.Interrupt, owesOwn func(*v1alpha1.Package) bool) (*v1alpha1.Interrupt, string) { | ||
| var ret *v1alpha1.Interrupt | ||
| var pack string | ||
|
|
||
|
|
@@ -3135,6 +3135,10 @@ func fudgeInterruptWithPriority(next []*v1alpha1.Package, configUpdates map[stri | |
| if _package.HasInterrupt() { | ||
| interrupts[_package.Name] = append(interrupts[_package.Name], _package.Interrupt) | ||
| } | ||
| } else if owesOwn != nil && owesOwn(_package) { | ||
| // a config change only brings the interrupts its keys match; a package whose own | ||
| // interrupt never ran on this node still owes it | ||
| interrupts[_package.Name] = append(interrupts[_package.Name], _package.Interrupt) | ||
|
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. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Copy the package interrupt before adding it to the merge. When this interrupt is the first selected 🤖 Prompt for AI Agents
|
||
| } | ||
| } | ||
|
|
||
|
|
||
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.
issue (non-blocking): The paragraph still opens with "A configMap change does not retry the interrupt either", which the new text contradicts.
With the flag set, the next sentence says the package's own interrupt "still runs after config", which is a retry. An admin who reads only the opening sentence and edits the ConfigMap of a package erroring at a
rebootinterrupt gets an unexpected reboot.Qualify the opening sentence, for example: "A configMap change retries the interrupt only on a node that still owes it."
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.
Good catch. The opening sentence now reads "A configMap change retries the interrupt only on a node that still owes it.", and the paragraph says when the flag is cleared (36df6a7).