Skyhook Version
operator/v0.19.0 (also reproduced on main at 4755b301)
Installation method
Helm
Kubernetes Version
n/a — reproduced in envtest
Component
Operator (controller-manager)
Describe the bug
A node whose taints are not tolerated by a NodeWright, and which is simultaneously held at waiting by cross-NodeWright sequencing, oscillates between waiting and blocked on every reconcile and never converges.
Two writers disagree within a single pass. RunSkyhookPackages calls IntrospectSkyhook (skyhook_controller.go:1321) and then SelectNodes (1326):
IntrospectNode forces StatusWaiting for any non-complete node that IsNodeReadyForSkyhook rejects (cluster_state_v2.go:1320-1324).
selectNodesWithCompartments then forces StatusBlocked because the node's taints are not tolerable.
Both SetStatus calls differ from the in-memory annotation, so node.updated latches even though the net persisted value is unchanged. SaveNodesAndSkyhook issues a Node PATCH and the reconcile requeues at 2s, and the next pass repeats identically — a no-op write plus a status update every 2 seconds, per affected node, indefinitely.
The ignore path had the same defect and was fixed in #586 by making the ignore label authoritative in IntrospectNode:
// Ignore overrides sequencing waits so a settled blocked node does not
// flip to Waiting and back to Blocked on every selection pass.
if !node.IsComplete() && CheckNodeIgnoreLabel(node) {
node.SetStatus(v1alpha1.StatusBlocked)
return node.Changed()
}
Taints need the equivalent guard. The awkward part is that IntrospectNode has no tolerations available: they are assembled in SelectNodes from Spec.AdditionalTolerations plus the reconciler's runtimeRequiredTolerations, so either those need plumbing through, or the scan in selectNodesWithCompartments needs to learn not to override a sequencing-held waiting.
Scope note. This is pre-existing, not introduced by #586, but #586 widens it. Before that PR, SetStatus(StatusBlocked) only ran for nodes GetNodesForNextBatch() actually returned, so the churn required the compartment to have batch capacity for the node. #586 moves the call into a scan over every node in every compartment, so the capacity precondition disappears. Measured with an identical probe on both commits:
| scenario |
base 4755b301 |
#586 head a9662b0c |
| batch has room for the node |
waiting -> blocked (churns) |
waiting -> blocked (churns) |
| batch soaked up by healthy nodes |
waiting -> waiting (stable) |
waiting -> blocked (churns) |
Steps to reproduce
In operator/internal/controller, build two NodeWrights over one node:
first with priority: 1, second with priority: 2, both selecting the node, each with one package and interruptionBudget.count: 1.
- Give the node a taint that neither NodeWright tolerates, e.g.
special=yes:NoSchedule.
- Leave
first incomplete on the node, so IsNodeReadyForSkyhook returns false for second.
- For
second, run IntrospectSkyhook(second, allSkyhooks, logger) followed by NewNodePicker(logger, nil).SelectNodes(second), three times, printing the node's status after each call.
Observed, on every pass:
pass1: introspect=waiting select=blocked
pass2: introspect=waiting select=blocked
pass3: introspect=waiting select=blocked
Expected: the node settles on one status and stops producing writes.
To see the widening specifically, add enough healthy filler nodes ahead of the tainted one to consume the batch. On 4755b301 the tainted node then stays waiting on every pass; on a9662b0c it still flips to blocked.
Additional context
The user-visible symptom beyond the write traffic is that the NodeWright carries a TaintNotTolerable condition naming the node while .status.nodeStatus for that node alternates, so tooling reading per-node status sees it flapping.
Raised as follow-up to the review on #586; that PR is otherwise good and the churn is not its doing.
Skyhook Version
operator/v0.19.0(also reproduced onmainat4755b301)Installation method
Helm
Kubernetes Version
n/a — reproduced in envtest
Component
Operator (controller-manager)
Describe the bug
A node whose taints are not tolerated by a NodeWright, and which is simultaneously held at
waitingby cross-NodeWright sequencing, oscillates betweenwaitingandblockedon every reconcile and never converges.Two writers disagree within a single pass.
RunSkyhookPackagescallsIntrospectSkyhook(skyhook_controller.go:1321) and thenSelectNodes(1326):IntrospectNodeforcesStatusWaitingfor any non-complete node thatIsNodeReadyForSkyhookrejects (cluster_state_v2.go:1320-1324).selectNodesWithCompartmentsthen forcesStatusBlockedbecause the node's taints are not tolerable.Both
SetStatuscalls differ from the in-memory annotation, sonode.updatedlatches even though the net persisted value is unchanged.SaveNodesAndSkyhookissues a Node PATCH and the reconcile requeues at 2s, and the next pass repeats identically — a no-op write plus a status update every 2 seconds, per affected node, indefinitely.The ignore path had the same defect and was fixed in #586 by making the ignore label authoritative in
IntrospectNode:Taints need the equivalent guard. The awkward part is that
IntrospectNodehas no tolerations available: they are assembled inSelectNodesfromSpec.AdditionalTolerationsplus the reconciler'sruntimeRequiredTolerations, so either those need plumbing through, or the scan inselectNodesWithCompartmentsneeds to learn not to override a sequencing-heldwaiting.Scope note. This is pre-existing, not introduced by #586, but #586 widens it. Before that PR,
SetStatus(StatusBlocked)only ran for nodesGetNodesForNextBatch()actually returned, so the churn required the compartment to have batch capacity for the node. #586 moves the call into a scan over every node in every compartment, so the capacity precondition disappears. Measured with an identical probe on both commits:4755b301a9662b0cwaiting -> blocked(churns)waiting -> blocked(churns)waiting -> waiting(stable)waiting -> blocked(churns)Steps to reproduce
In
operator/internal/controller, build two NodeWrights over one node:firstwithpriority: 1,secondwithpriority: 2, both selecting the node, each with one package andinterruptionBudget.count: 1.special=yes:NoSchedule.firstincomplete on the node, soIsNodeReadyForSkyhookreturns false forsecond.second, runIntrospectSkyhook(second, allSkyhooks, logger)followed byNewNodePicker(logger, nil).SelectNodes(second), three times, printing the node's status after each call.Observed, on every pass:
Expected: the node settles on one status and stops producing writes.
To see the widening specifically, add enough healthy filler nodes ahead of the tainted one to consume the batch. On
4755b301the tainted node then stayswaitingon every pass; ona9662b0cit still flips toblocked.Additional context
The user-visible symptom beyond the write traffic is that the NodeWright carries a
TaintNotTolerablecondition naming the node while.status.nodeStatusfor that node alternates, so tooling reading per-node status sees it flapping.Raised as follow-up to the review on #586; that PR is otherwise good and the churn is not its doing.