You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 09df69e
Browse filesBrowse the repository at this point in the historyBrowse files
fix(backups): bound Kafka metadata run at the Job layer, not a controller wall clock
The r5 running-Job deadline was enforced in reconcileKafka /
reconcileKafkaRestore: at kafkaDefaultBackupDeadline from StartedAt the
BackupJob/RestoreJob was marked Failed. But the batch Job carried no
activeDeadlineSeconds and markBackupJobFailed only writes status, so the
Job kept running, finished, and its final `s3 PUT` uploaded the object —
while the terminal phase meant no Backup was ever created and cleanup
(keyed on Backup deletion) never ran. Every timed-out run thus orphaned a
uniquely-keyed object, and a cluster whose export legitimately exceeds the
deadline could never produce a usable backup. The shipped docs also
claimed the run "keeps the BackupJob Running ... rather than failing on a
wall clock", which the deadline made false.
Move the bound onto the Job as activeDeadlineSeconds (setKafkaJobDeadline):
Kubernetes fails an unschedulable or wedged Job as DeadlineExceeded and
kills its pod before the upload, so a timed-out run lands in the JobFailed
branch cleanly and leaves no object behind. Make it operator-tunable with
the `backupTimeout` BackupClass parameter (a Go duration), which
round-trips through the Backup so a restore is bounded by the same value;
the readiness-precondition wait honours it too. Correct the docs.
Also requeue on a transient broker-image List error instead of failing the
whole run: resolveKafkaBrokerImage now wraps a sentinel (errNoKafkaBroker)
so only a Ready cluster with no listable broker is terminal.
Tests: pin the deadline on the built Job, the backupTimeout override, a
JobFailed run marking the owner Failed, and the transient-List requeue.
Signed-off-by: Andrey Kolkov <[email protected]>
Assisted-by: LLM
Copy file name to clipboardExpand all lines: docs/operations/backup-classes.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -361,4 +361,4 @@ This is the "reconstruct a cluster's topic topology" flow: an in-place restore r
361
361
362
362
One consequence of the Admin-API approach: if a topic is declared in the Kafka app's `spec.topics`, the Strimzi Topic Operator owns it and reconciles its config from the `KafkaTopic` CR, reverting any dynamic config an in-place restore sets that the CR does not list. So this strategy is durable for **out-of-band** topics (created directly, no CR); for CR-declared topics the `KafkaTopic` CR / GitOps is the source of truth. A to-copy restore onto a fresh empty cluster is unaffected. For message-data durability, use a volume-snapshot strategy instead. See `examples/backups/kafka-metadata/` for the end-to-end flow.
363
363
364
-
Restore is not transactional: it applies topics one at a time as it reads the object, so a failure part-way through (an RF mismatch, an unreachable broker) leaves the topics already created behind — re-running the restore is safe (existing topics are reconciled, not recreated). Like the Rabbitmq driver, this one owns its object outright and deletes it from the bucket when its `Backup` is deleted — via a one-shot Job the Backup's removal waits on — so a retention-pruned `Plan` does not accumulate objects; the delete is **best-effort** with the same give-up conditions and `backups.cozystack.io/skip-artifact-cleanup` escape hatch documented for Rabbitmq above. One caveat specific to this driver: its image ships an older `curl` (7.76.1) whose `--aws-sigv4` omits a non-default port from the SigV4 canonical host, which every S3 backend then rejects with `403 SignatureDoesNotMatch` on a ported endpoint (fixed in curl 7.86.0). The strategy script works around it with `--connect-to` — it signs and sends a portless URL and redirects the connection to the real port — so a ported endpoint (the `backupStorage.endpoint` fallback `…:8333`, `provisionBucket: false`, or SeaweedFS with `s3.ingress.enabled: false`) works too; the sibling drivers run curl 8.x and need no such workaround. Export time scales with topic count — the backup spends two Kafka CLI invocations (each a fresh JVM, ~1.5–3s) per topic — so a cluster with many hundreds of topics takes tens of minutes; the Job inherits the shared Job-strategy pod (no `activeDeadlineSeconds`, like every Job-shaped driver), so a wedged export keeps the BackupJob `Running` until the operator intervenes rather than failing on a wall clock.
364
+
Restore is not transactional: it applies topics one at a time as it reads the object, so a failure part-way through (an RF mismatch, an unreachable broker) leaves the topics already created behind — re-running the restore is safe (existing topics are reconciled, not recreated). Like the Rabbitmq driver, this one owns its object outright and deletes it from the bucket when its `Backup` is deleted — via a one-shot Job the Backup's removal waits on — so a retention-pruned `Plan` does not accumulate objects; the delete is **best-effort** with the same give-up conditions and `backups.cozystack.io/skip-artifact-cleanup` escape hatch documented for Rabbitmq above. One caveat specific to this driver: its image ships an older `curl` (7.76.1) whose `--aws-sigv4` omits a non-default port from the SigV4 canonical host, which every S3 backend then rejects with `403 SignatureDoesNotMatch` on a ported endpoint (fixed in curl 7.86.0). The strategy script works around it with `--connect-to` — it signs and sends a portless URL and redirects the connection to the real port — so a ported endpoint (the `backupStorage.endpoint` fallback `…:8333`, `provisionBucket: false`, or SeaweedFS with `s3.ingress.enabled: false`) works too; the sibling drivers run curl 8.x and need no such workaround. Export time scales with topic count — the backup spends two Kafka CLI invocations (each a fresh JVM, ~1.5–3s) per topic — so a cluster with many hundreds of topics takes tens of minutes; the run is bounded at the Job layer by `activeDeadlineSeconds` (default 30 minutes, from the same wait the readiness precondition uses), so a wedged or unschedulable run is failed by Kubernetes as `DeadlineExceeded` — the pod is killed before the script's final upload, so no object is left orphaned — rather than requeuing forever. A cluster whose export legitimately runs longer raises the bound with the `backupTimeout` BackupClass parameter (a Go duration, e.g. `2h`), which round-trips through the `Backup` so a restore is bounded by the same value.
returnr.markBackupJobFailed(ctx, j, fmt.Sprintf("Kafka metadata backup Job did not complete within %s (its pod may be unschedulable)", kafkaDefaultBackupDeadline))
367
-
}
397
+
// The Job carries activeDeadlineSeconds (set in ensureKafkaJob), so a run
398
+
// that never completes - an unschedulable pod or a wedged export - is
399
+
// failed by Kubernetes as DeadlineExceeded and lands in the JobFailed
400
+
// branch above; the pod is killed before the script's final upload, so no
401
+
// object is left orphaned. Here we only poll while it makes progress.
returnr.markRestoreJobFailed(ctx, restoreJob, fmt.Sprintf("Kafka metadata restore Job did not complete within %s (its pod may be unschedulable)", kafkaDefaultBackupDeadline))
647
-
}
691
+
// Same bound as the backup path, at the Job layer: the restore Job carries
692
+
// activeDeadlineSeconds, so an unschedulable or wedged pod is failed by
693
+
// Kubernetes and lands in the JobFailed branch above rather than requeuing
0 commit comments