Skip to content

fix: Background-propagation cleanup (#30) + configurable backoffLimit (#31)#33

Merged
sionsmith merged 3 commits into
mainfrom
fix/issue-30-orphaned-pods
Jun 12, 2026
Merged

fix: Background-propagation cleanup (#30) + configurable backoffLimit (#31)#33
sionsmith merged 3 commits into
mainfrom
fix/issue-30-orphaned-pods

Conversation

@sionsmith

@sionsmith sionsmith commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Fixes #30 and #31. Stacked on #32 — merge that first; this PR then reduces to the two commits here (releases as 0.2.10).

#30 — orphaned pods after CR deletion

Root cause: finalizer cleanup deleted Jobs with DeleteParams::default() — no propagationPolicy. The batch/v1 Job API is a legacy API whose server-side default deletion propagation is Orphan, so the garbage collector strips the Job ownerReference from the pods instead of deleting them — exactly the reporter's "ownerReference exists then vanishes". kubectl masks this by always sending Background; bare API clients hit the legacy default.

Fix: all cleanup deletes (restore Jobs/ConfigMap, backup Jobs/CronJob/ConfigMap) go through a shared cleanup_delete_params() helper requesting Background propagation.

Verification: reproduced on kind with the released operator (pod ownerReference present before deletion, stripped after; Completed pod left behind). With the fix: deleting a KafkaRestore and a one-shot KafkaBackup removed Jobs and pods.

#31 — configurable backoffLimit, single-attempt restores by default

Reproduction: generated restore Job had hardcoded backoffLimit: 3; one failing KafkaRestore produced 4 pod attempts. Restores append/purge target topics, so implicitly retrying a partially completed attempt can duplicate data.

Fix: new spec.backoffLimit (minimum 0) on both KafkaRestore and KafkaBackup, plumbed into the restore Job, one-shot backup Job, and scheduled CronJob template.

Behavior change (deliberate): restore Jobs now default to backoffLimit: 0 — exactly one attempt per restore, retries opt-in — matching the reporter's intent ("I personally want to be intentional for each run"). Backup Jobs keep the previous default of 3 (backups are safe to retry).

Verification: TDD (4 red tests against hardcoded behavior, then green; 37 integration tests total). E2E on kind: default restore → Job backoffLimit=0, exactly 1 pod attempt, terminal RestoreFailed; spec.backoffLimit: 2 → exactly 3 attempts; no Job re-creation (#32 interplay) and no orphaned pods on deletion (#30 interplay). CRDs regenerated and helm chart copies synced.

🤖 Generated with Claude Code

sionsmith and others added 3 commits June 12, 2026 06:56
A finished Job has status.active=0, which is_job_running() read as "no
job running", so every 5-minute requeue created a fresh restore Job —
and the completion check ran only after the creation step, so the
duplicate was already spawned by the time the status flipped to
RestoreComplete. Terminally failed Jobs were re-created forever, and
the one-shot KafkaBackup path had the identical defect.

- Add jobs::job_state::classify_jobs(): aggregate the full set of Jobs
  for a CR into Succeeded/InProgress/Failed/NoJobs using Job conditions
  (Complete/Failed), so pod retries within backoffLimit stay InProgress
  and a pending Job with no status yet blocks duplicate creation.
- Gate Job creation on that classification: restores never create a
  second Job; one-shot backups only re-run via the manual trigger
  annotation, which still never stacks onto an active Job.
- Watch owned Jobs (.owns) in both controllers so status reflects
  completion/failure within seconds instead of the next requeue.
- Report a terminal RestoreFailed condition for failed restore Jobs
  instead of silently re-creating them.
- Make status patches idempotent (Job-derived timestamps, skip when the
  condition/lastBackup already reflects the outcome) so the new watch
  cannot churn lastTransitionTime in a reconcile loop.

Fixes #29

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Deleting a KafkaRestore (or KafkaBackup) removed its Jobs but left the
Jobs' pods behind: the finalizer cleanup deleted Jobs with
DeleteParams::default(), and the batch/v1 Job API's legacy server-side
default deletion propagation is Orphan, so the garbage collector
stripped the Job ownerReference from the pods instead of deleting them
(kubectl masks this by always sending Background propagation).

Cleanup now deletes Jobs, CronJobs, and ConfigMaps through a shared
cleanup_delete_params() helper that requests Background propagation,
so dependents are garbage collected with their parents.

Fixes #30

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Jobs generated for KafkaRestore and KafkaBackup hardcoded
backoffLimit: 3, so a failing restore made up to 4 pod attempts.
Restores append to or purge target topics, which makes implicit
retries of a partially completed attempt a data-duplication hazard —
each run should be intentional.

- Add spec.backoffLimit (minimum 0) to both CRDs, plumbed into the
  restore Job, one-shot backup Job, and scheduled CronJob template.
- Restore Jobs now default to backoffLimit 0 (exactly one attempt);
  retries are opt-in. Backup Jobs keep the previous default of 3.

Fixes #31

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sionsmith sionsmith changed the title fix: delete cleanup resources with Background propagation fix: Background-propagation cleanup (#30) + configurable backoffLimit (#31) Jun 12, 2026
@sionsmith sionsmith merged commit a557730 into main Jun 12, 2026
7 checks passed
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.

KafkaRestore orphaned pods

1 participant