Skip to content

fix(operator): requeue when an owned ConfigMap sync is deferred#266

Merged
lockwobr merged 1 commit into
mainfrom
worktree-config-bug
Jun 4, 2026
Merged

fix(operator): requeue when an owned ConfigMap sync is deferred#266
lockwobr merged 1 commit into
mainfrom
worktree-config-bug

Conversation

@lockwobr

@lockwobr lockwobr commented Jun 3, 2026

Copy link
Copy Markdown
Collaborator

Description

Fixes the permanent ConfigMap desync described in #245, which also surfaces as the flaky config-skyhook glob update chainsaw step.

Root cause

HandleConfigUpdates only writes the owned ConfigMap inside the completedNodes == nodeCount gate. When that gate is closed on the reconcile that observes the diff, the write is deferred and the only fallback is the 10m MaxInterval requeue.

The trigger in #245: a stale Status.ConfigUpdates entry left by a prior config-only spec change matches a newly-added configInterrupt glob, flipping HasInterrupt true. That drives a spurious interrupt cycle which holds the gate closed for the whole cycle, leaving a single race-prone reconcile (post-interrupt pod deletion vs. informer-cache convergence) to catch the gate open. Miss it and the CM stays diverged from spec for up to 10 minutes while status reads complete — and the chainsaw step times out at 270s, hence the CI flake.

Fix (issue Option B)

  • Surface a pendingSync signal from HandleConfigUpdates / UpsertConfigmaps: a CM diff was observed but the write was deferred because the gate was closed.
  • Shorten the otherwise idle requeue from MaxInterval (10m) to a new configSyncRetryInterval (30s) so the deferred write retries promptly. Extracted into a small reconcileResult helper (keeps Reconcile under the gocyclo threshold and makes the clamp unit-testable).
  • pendingSync deliberately does not short-circuit the reconcile: progression toward the gate opening happens in processSkyhooksPerNode, so an early return would deadlock (the gate would never open). It only influences the final idle requeue.

Interrupt semantics, Status.ConfigUpdates clearing, and static-interrupt suppression are untouched, so there's no risk of spurious reboots from this change.

Note on the issue's preferred Option A (eager CM write): I verified it breaks the update-while-running feature — writing the CM eagerly destroys the diff signal the gated path relies on to record changed keys and fire the config-interrupt, silently disabling config-interrupts for changes applied while a node is busy. Hence Option B.

A deeper follow-up remains: Status.ConfigUpdates does double duty (changed-keys + static-interrupt suppression), which is what lets a stale entry trigger a spurious cycle. This PR makes that benign (the CM always converges); eliminating the spurious interrupt itself is worth a separate design discussion.

Tests

  • New unit tests (TDD): HandleConfigUpdates signals / doesn't signal pendingSync; validateAndUpsertSkyhookData does not short-circuit on a deferred sync (deadlock guard); pure reconcileResult clamp test.
  • Full controller suite: 166/166 pass. make vet + golangci-lint: 0 issues.
  • The config-skyhook glob update chainsaw step is the e2e guard for this fix.

Docs

No docs/ change: this alters only requeue cadence (no CRD field, CLI, annotation, metric, or flag), and no doc page covers config-update internals.

Checklist

  • I am familiar with the Contributing Guidelines.
  • My commits are signed off (git commit -s) per the DCO.
  • New or existing tests cover these changes.
  • The documentation is up to date with these changes.

Closes #245

@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR tracks deferred owned-ConfigMap writes behind the completed-nodes gate and requeues sooner when such a sync is pending. HandleConfigUpdates now returns (updated, pendingSync, error) and defers applying a ConfigMap when it diverges but no node has completed the package, signaling pendingSync=true. UpsertConfigmaps and validateAndUpsertSkyhookData propagate pendingSync without short-circuiting reconciles. The reconciler records configSyncPending and reconcileResult requeues after 30s when idle and a sync is pending.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • mskalka
  • rice-riley
  • ayuskauskas
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: deferring ConfigMap syncs is now detected and triggers a requeue mechanism, directly addressing the permanent desync issue in #245.
Description check ✅ Passed The description clearly relates to the changeset, explaining the root cause of the desync, the fix implementation (Option B), test coverage, and connecting it to issue #245.
Linked Issues check ✅ Passed The PR fully implements Option B from issue #245: adds pendingSync signal from HandleConfigUpdates/UpsertConfigmaps, introduces configSyncRetryInterval for prompt retries, and ensures deferred syncs don't short-circuit reconciliation, directly addressing all stated objectives.
Out of Scope Changes check ✅ Passed All changes are scoped to the requirements in #245: ConfigMap sync deferral tracking, requeue interval adjustment, and related tests. Interrupt semantics, Status.ConfigUpdates clearing, and static-interrupt suppression remain unchanged as intended.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-config-bug

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@operator/internal/controller/skyhook_controller.go`:
- Around line 1394-1395: The error returned when updating the config map uses
fmt.Errorf("...%s", err) which prevents proper error wrapping; change the
fmt.Errorf call that constructs the error for the configmap update failure to
use the %w verb (e.g., fmt.Errorf("error updating config map [%s]: %w",
newCM.Name, err)) so callers can use errors.Is/errors.As to inspect the
underlying error; locate the fmt.Errorf that references newCM.Name in
skyhook_controller.go and replace %s with %w (no other logic changes required).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: b1cb75fe-0f37-415b-9442-c80628578df6

📥 Commits

Reviewing files that changed from the base of the PR and between b6aa842 and c9feb43.

📒 Files selected for processing (2)
  • operator/internal/controller/skyhook_controller.go
  • operator/internal/controller/skyhook_controller_test.go

Comment thread operator/internal/controller/skyhook_controller.go Outdated
@coveralls

coveralls commented Jun 4, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 26922809437

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Warning

No base build found for commit b6aa842 on main.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 81.591%

Details

  • Patch coverage: 7 uncovered changes across 1 file (51 of 58 lines covered, 87.93%).

Uncovered Changes

File Changed Covered %
operator/internal/controller/skyhook_controller.go 58 51 87.93%

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 9463
Covered Lines: 7721
Line Coverage: 81.59%
Coverage Strength: 19.22 hits per line

💛 - Coveralls

HandleConfigUpdates only writes the owned ConfigMap inside the
completedNodes == nodeCount gate. When the gate is closed on the reconcile
that observes the diff (e.g. a stale Status.ConfigUpdates entry matching a
new configInterrupt glob drives a spurious interrupt cycle), the write is
deferred and the only fallback is the 10m MaxInterval requeue. The CM can
stay diverged from spec for minutes while status reads complete, and the
config-skyhook glob-update chainsaw step flakes when the one race-prone
post-interrupt window is missed.

Surface a pendingSync signal from HandleConfigUpdates/UpsertConfigmaps and
shorten the otherwise idle requeue from MaxInterval to a 30s
configSyncRetryInterval so the deferred write retries promptly. pendingSync
deliberately does not short-circuit the reconcile: progression toward the
gate opening happens in processSkyhooksPerNode, so an early return would
deadlock. The interrupt semantics and Status.ConfigUpdates handling are
unchanged.

Closes #245

Signed-off-by: Brian Lockwood <[email protected]>
@lockwobr lockwobr force-pushed the worktree-config-bug branch from c9feb43 to d03d82a Compare June 4, 2026 00:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
operator/internal/controller/skyhook_controller.go (1)

1218-1255: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Wrap early error returns in HandleConfigUpdates with operation context
The return false, false, err statements at lines 1220, 1247, and 1254 return a bare err, losing reconcile context.

♻️ Proposed fix
 			exists, err := r.PodExists(ctx, node.GetNode().Name, skyhook.GetSkyhook().Name, &_package)
 			if err != nil {
-				return false, false, err
+				return false, false, fmt.Errorf("checking existing pod for node %s package %s: %w",
+					node.GetNode().Name, _package.GetUniqueName(), err)
 			}
@@
 						if err != nil {
-							return false, false, err
+							return false, false, fmt.Errorf("listing pods for node %s package %s: %w",
+								node.GetNode().Name, _package.GetUniqueName(), err)
 						}
@@
 								err := r.Delete(ctx, &pod)
 								if err != nil {
-									return false, false, err
+									return false, false, fmt.Errorf("deleting pod %s/%s for node %s package %s: %w",
+										pod.Namespace, pod.Name, node.GetNode().Name, _package.GetUniqueName(), err)
 								}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@operator/internal/controller/skyhook_controller.go` around lines 1218 - 1255,
The early returns in HandleConfigUpdates currently return raw errors from calls
to PodExists, dal.GetPods, and r.Delete (see PodExists, r.dal.GetPods, and
r.Delete usage) which loses reconcile context; update each return to wrap the
error with contextual information (e.g., include operation name
"HandleConfigUpdates" plus node name and package unique name or action being
performed) using fmt.Errorf or errors.Wrap so the error becomes something like
"HandleConfigUpdates: failed to <action> for node <node.GetNode().Name> package
<_package.GetUniqueName()>: %w" before returning; apply this to the three spots
where return false, false, err appears.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@operator/internal/controller/skyhook_controller.go`:
- Around line 1218-1255: The early returns in HandleConfigUpdates currently
return raw errors from calls to PodExists, dal.GetPods, and r.Delete (see
PodExists, r.dal.GetPods, and r.Delete usage) which loses reconcile context;
update each return to wrap the error with contextual information (e.g., include
operation name "HandleConfigUpdates" plus node name and package unique name or
action being performed) using fmt.Errorf or errors.Wrap so the error becomes
something like "HandleConfigUpdates: failed to <action> for node
<node.GetNode().Name> package <_package.GetUniqueName()>: %w" before returning;
apply this to the three spots where return false, false, err appears.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: fb0a907d-b1d0-496b-b486-0c4a4d7c399d

📥 Commits

Reviewing files that changed from the base of the PR and between c9feb43 and d03d82a.

📒 Files selected for processing (2)
  • operator/internal/controller/skyhook_controller.go
  • operator/internal/controller/skyhook_controller_test.go

@lockwobr lockwobr enabled auto-merge (squash) June 4, 2026 01:11
@lockwobr lockwobr self-assigned this Jun 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Owned ConfigMap can permanently desync from spec when a stale Status.ConfigUpdates entry drives a spurious interrupt cycle

3 participants