🐛 fix(controller): snapshot CronJob drops scheduling constraints and wedges permanently - #5
Merged
Merged
Conversation
…pshot CronJob The snapshot CronJob built its pod without NodeSelector, Tolerations or Affinity, even though the CRD accepts all three and the StatefulSet honours them. On clusters with tainted node pools the snapshot pod is unschedulable and pends forever, and no value in the CR can work around it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ConcurrencyPolicy was hardcoded to Forbid with no timeouts anywhere, so a snapshot job whose pod could never be scheduled stayed Active forever and every subsequent run was skipped permanently. Forbid stays the default: it prevents overlapping CREATE SNAPSHOT runs, which Replace would not. The wedge is broken by activeDeadlineSeconds instead, which fails a stuck job and frees the slot without killing a healthy job that simply needs more time. startingDeadlineSeconds separately bounds the missed-start lookback so the ">100 missed start times" lockout cannot trigger. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nJobs are repaired needsUpdate compared only the schedule string and the container count, so adding tolerations to the built CronJob changed neither and already-deployed CronJobs would silently keep their broken pod template after upgrade. Any cluster that already has a CronJob would see no effect from the scheduling fix without this. Only operator-managed fields are compared: comparing whole specs would report drift on every reconcile, because the API server defaults many PodSpec fields the operator never sets. TestSnapshotCronJobNeedsUpdateIgnoresServerDefaults guards against that regression. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thilak009
force-pushed
the
fix/snapshot-cronjob-scheduling
branch
from
August 17, 2026 02:08
be1de5f to
de9a37d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The snapshot CronJob can never run on a cluster whose nodes carry taints, and once it fails the first time it never recovers. The CronJob object looks perfectly healthy in
kubectl get cronjobthe whole time —.status.lastSuccessfulTimesimply stays empty forever.Two compounding defects:
1. Scheduling fields are dropped.
buildSnapshotCronJobbuilt its pod withoutNodeSelector,TolerationsorAffinity, even though the CRD accepts all three andbuildStatefulSethonours them (statefulset.go:147-149). On tainted node pools the snapshot pod is unschedulable and pends forever:Nothing in the CR could work around this —
spec.tolerationsreached the StatefulSet, but never the CronJob. So the memgraph pods schedule fine while the snapshot pod cannot.2.
Forbidmade it permanent.ConcurrencyPolicywas hardcoded toForbidwith noactiveDeadlineSecondsorstartingDeadlineSecondsanywhere. A permanently-Pendingjob staysActiveforever, so every later tick is skipped.lastScheduleTimealso stops advancing underForbid, which eventually trips the CronJob controller'stoo many missed start times (> 100)lockout — a second, independent wedge.Changes
snapshot.go— propagateNodeSelector/Tolerations/Affinityto the snapshot pod template, matching the StatefulSet.memgraphcluster_types.go— three new optionalSnapshotSpecfields:concurrencyPolicy(defaultForbid),activeDeadlineSeconds(default 600),startingDeadlineSeconds(default 300).Forbidis kept as the default rather than switching toReplace: it prevents overlappingCREATE SNAPSHOTruns, whereasReplacewould kill a legitimately slow snapshot on every tick and never let one finish — trading "never runs" for "never finishes". The wedge is broken byactiveDeadlineSecondsinstead: a stuck job is marked Failed and its pods terminated, freeing the slot so the next tick runs, without disturbing a healthy job that simply needs more time.concurrencyPolicyis exposed for anyone who does wantReplace.snapshot.go— replace the update check. Without this the rest of the PR has no effect on any existing deployment.needsUpdatecompared only the schedule string and the container count; adding tolerations changes neither, so any cluster that already has a CronJob would silently keep its broken pod template after upgrade.The new
snapshotCronJobNeedsUpdatecompares only operator-managed fields. Comparing whole specs would report drift on every reconcile, because the API server defaults manyPodSpecfields the operator never sets;TestSnapshotCronJobNeedsUpdateIgnoresServerDefaultsguards against that update loop.Compatibility
No CronJobs are deleted by this PR — it only repairs existing ones.
The CRD change is purely additive (three new optional fields with defaults), so the updated CRD must be installed before the operator writes them.
Upgrade note
activeDeadlineSecondsis baked into a Job at creation, so it does not apply retroactively to a Job that is already stuckActive. UnderForbidsuch a Job keeps blocking even after this ships, and needs a one-time manual delete:If the CronJob still refuses to schedule afterwards, check its events for
too many missed start times (> 100)—startingDeadlineSecondsprevents this recurring, but a CronJob already in that state may need recreating.When auditing whether snapshots have ever worked, check
lastSuccessfulTimerather than whether the object exists:An empty value means it has never run once.
Testing
make cipasses — lint 0 issues, all packages green, build succeeds. New coverage: scheduling-field propagation, deadline defaults and overrides, update-detection drift cases plus the server-defaults loop guard.Follow-up
A second PR handles the other half of this:
snapshot.enabled: falseis currently unsettable (non-pointerbool+omitempty+default=true) and the CronJob cleanup path is unreachable. Split out because it changes behaviour on existing clusters.Also not addressed here:
spec.snapshot.retentionCountandspec.snapshot.s3.retentionDaysare accepted by the CRD and read by no controller code.🤖 Generated with Claude Code