improvement: commit + log + metric on task failure in GC and lifecycle cold-archive#2771
Open
delthas wants to merge 1 commit into
Open
improvement: commit + log + metric on task failure in GC and lifecycle cold-archive#2771delthas wants to merge 1 commit into
delthas wants to merge 1 commit into
Conversation
Contributor
Hello delthas,My role is to assist you with the merge of this Available options
Available commands
Status report is not available. |
Contributor
Waiting for approvalThe following approvals are needed before I can proceed with the merge:
|
Codecov Report❌ Patch coverage is
Additional details and impacted files
... and 5 files with indirect coverage changes
@@ Coverage Diff @@
## development/9.5 #2771 +/- ##
===================================================
- Coverage 75.40% 75.28% -0.12%
===================================================
Files 201 201
Lines 13868 13888 +20
===================================================
- Hits 10457 10456 -1
- Misses 3401 3422 +21
Partials 10 10
Flags with carried forward coverage won't be shown. Click here to find out more. 🚀 New features to boost your workflow:
|
benzekrimaha
reviewed
Jul 8, 2026
benzekrimaha
reviewed
Jul 8, 2026
benzekrimaha
reviewed
Jul 8, 2026
benzekrimaha
reviewed
Jul 8, 2026
c7a81fb to
8051dd1
Compare
…e cold-archive
Two error paths returned { committable: false } to the consumer after
their retry budget was exhausted, then never called onEntryCommittable.
The offset never advanced past these entries and they stayed in the
OffsetLedger indefinitely. With the BB-777 rebalance-wait-for-drain fix,
this now blocks every rebalance until the outer timeout fires and the
pod restarts, instead of just leaking memory.
Neither flow is retriggered by an upstream cron. The cold-status archive
is a one-shot notification from the cold backend, and LifecycleTask
skips objects with transitionInProgress=true on subsequent scans, so
there is no automatic recovery either way. There is also no dead-letter
mechanism today.
Step 1 is to stop pretending: commit the offset on permanent failure,
log an error message at the point where we drop the entry (with the
individual data-store locations / parts so ops can reclaim manually),
and increase a per-extension counter so operators can query it.
- GarbageCollectorTask._deleteArchivedSourceData: move the log + metric
to the outer retry-wrapper's completion callback so it fires once per
final give-up rather than per attempt. Simplify the ObjNotFound /
NoSuchBucket carveout to done(err) (same semantics). Stash the parts
fetched from metadata on the entry (target.locations) so the outer
failure log can surface them.
- GarbageCollectorTask._executeDeleteData: same treatment for the hot
transition / restore-expiration paths (already committed on failure
but silently). Parts are already on the entry, log them.
- LifecycleObjectTransitionProcessor.processColdStorageStatusEntry:
same idea, in the caller of retryWrapper.retry. Uses
this.getProcessorType() rather than a hard-coded string.
- Add s3_gc_failed_total and s3_lifecycle_failed_total counters with
origin / location labels.
- Unit tests: retry tests assert the metric fires with expected args;
three new tests on LifecycleObjectTransitionProcessor cover the
processor's failure metric under retryable=true / retryable=false /
success. Fast retry backoff in the fixtures so they run in ms.
Issue: BB-772
8051dd1 to
ed4e278
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.
TL;DR — Three task error paths returned
{ committable: false }after their retry budget was exhausted without ever callingonEntryCommittable. The offset never advanced past those entries, they lingered in theOffsetLedger, and with the BB-777 rebalance-wait-for-drain fix now merged this turns every rebalance that catches one into a forced pod restart. This PR flips those sites to commit on final failure, emits a structured error log at the point of drop (including the individual data-store parts so ops can reclaim manually), and adds a per-extension counter that can be queried from Prometheus.What "final failure" means here
All three sites are wrapped by a
BackbeatTask.retry-style helper with a ~5 minute budget and exponential backoff — they only reach the poison-pill path once retries are exhausted or the error is flagged non-retryable. Realistic triggers include:AccessDeniedafter a role rotation,ConflictonputMetadata,InvalidRequest)arsenal.errors.InternalErrorwhen the vault-derived client can't be obtainedWhy not "kafka will redeliver"
That was the intent behind
committable: false, but it doesn't work: Kafka only redelivers on the next assignment (rebalance / restart), and neither path benefits from an upstream cron. The cold-status message is a one-shot notification from the cold backend; the GCdeleteArchivedSourceDataentry is published exactly once by the archive task in fire-and-forget mode. On top of that,LifecycleTaskexplicitly skips objects withtransitionInProgress=trueon later bucket scans, so the regular pipeline won't pick a stuck object back up either. Effectively the current behavior is "hold the offset forever, ledger grows, and next rebalance forces a pod restart."What this PR does
For each of the three sites:
{ committable: false }— just calldone(err)so the consumer commits the offset like it would for any other completed task.s3_gc_failed_total{origin, location}for GCs3_lifecycle_failed_total{origin, type, location}for lifecycleThe log + metric live at the retry-wrapper's completion callback (not inside the task's own error handler) so they fire exactly once per final give-up, not once per attempt.
Sites covered:
GarbageCollectorTask._deleteArchivedSourceData— the archive-side warm-copy cleanup. Parts come fromobjMD.getLocation()inside the async.waterfall; stashed on the entry viaentry.setAttribute('target.locations', ...)so the outer failure log can surface them.GarbageCollectorTask._executeDeleteData— the hot transition / restore-expiration paths. Already committed silently on failure (no poison-pill), but had no log or metric. Parts are already on the entry.LifecycleObjectTransitionProcessor.processColdStorageStatusEntry— the cold-status archive handler. No parts concept (this task manipulates metadata + enqueues GC); log fires without a parts field. Usesthis.getProcessorType()for the metric label rather than a hard-coded string.For GC, the pre-existing
ObjNotFound/NoSuchBucketcarveout (return with commit, no retry) is simplified fromdone(err, { committable: true })todone(err)— same semantics, less noise.What this PR does not do
ReplicateObject,MultipleBackendTask,CopyLocationTask,LifecycleUpdateTransitionTask) that usecommittable: falsecorrectly with a producer-callbackonEntryCommittable.Issue: BB-772