[fix][broker] Fix bucket snapshot trim and segment loading - #26280
[fix][broker] Fix bucket snapshot trim and segment loading#26280nodece wants to merge 4 commits into
Conversation
2d9f628 to
05844ff
Compare
void-ptr974
left a comment
There was a problem hiding this comment.
Two edge cases remain around resolving the bucket ID.
| .orElseGet(() -> CompletableFuture.completedFuture(getAndUpdateBucketId())); | ||
|
|
||
| return bucketIdFuture.thenCompose(bucketId -> | ||
| executeWithRetry(() -> ctx.bucketSnapshotStorage().deleteBucketSnapshot(bucketId), |
There was a problem hiding this comment.
Nice improvement to make deletion wait for snapshot creation; it closes the missing bucketId race. Two edge cases remain:
- If snapshot creation fails after trim has started,
afterCreateImmutableBucket()resolves the future withINVALID_BUCKET_ID(-1), and this path callsdeleteBucketSnapshot(-1). A storage failure would stop the sequential trim chain and skip the subsequent merge. Since no snapshot was persisted, storage deletion should be skipped while orphan-bucket cleanup continues. - When no creation future exists,
getAndUpdateBucketId()is evaluated beforecompletedFuture()is created. A missing or malformed cursor property therefore throws synchronously. The failure should instead be propagated through the returnedCompletableFutureso callers can handle it consistently.
There was a problem hiding this comment.
Thanks for the detailed review. Addressed both points in the latest commits:
asyncDeleteBucketSnapshot()now resolves bucket id viaOptionaland skips storage deletion when snapshot creation failed, so trim does not calldeleteBucketSnapshot(-1)and the chain is not broken by the old sentinel path.- The no-create-future path now catches
getAndUpdateBucketId()failures and returns a failedCompletableFutureinstead of throwing synchronously, so callers handle failures consistently through the async chain.
| })) | ||
| .orElseGet(() -> CompletableFuture.completedFuture(getAndUpdateBucketId())); | ||
|
|
||
| return bucketIdFuture.thenCompose(bucketId -> |
There was a problem hiding this comment.
Waiting for snapshot creation here resolves the missing bucketId race condition, but the trim decision itself is made earlier in BucketDelayedDeliveryTracker.asyncTrimImmutableBuckets(). Once this future completes, the range is no longer revalidated—neither to confirm it is still orphaned nor to verify that the same bucket remains mapped to the selected range.
#26260 performed both checks after the snapshot‑create future completed, and this PR is intended to subsume that fix. In the current implementation, a cursor reset, backward movement of the mark‑delete position, or a bucket mapping change while snapshot creation is pending can make the original trim decision stale before storage deletion begins.
Could this lifecycle coordination be kept within the tracker—revalidating firstActiveLedgerId() and the exact range‑to‑bucket identity before starting deletion? Ideally, the ownership/eligibility guard should also cover the asynchronous delete completion before tracker state is removed.
There was a problem hiding this comment.
Good point on stale trim decisions. The tracker now keeps lifecycle coordination in BucketDelayedDeliveryTracker: it revalidates exact range->bucket ownership before starting deletion and revalidates ownership again before removing tracker state in the async completion. This prevents deleting/removing state for a bucket that has been replaced while delete is in flight.
| .attr("bucketKey", bucketKey) | ||
| .exception(ex) | ||
| .log("Failed to delete bucket snapshot"); | ||
| CompletableFuture<Long> bucketIdFuture = getSnapshotCreateFuture() |
There was a problem hiding this comment.
The PR description states that buckets with unfinished snapshot creation are skipped, but the current implementation instead waits for the creation future. Since asyncTrimImmutableBuckets() is part of the global trimFuture chain, a slow or stalled snapshot creation can keep trimFuture.isDone() false, which blocks subsequent trim/merge attempts and causes clear() to wait on the same future.
The latest commit also removed the prior isDone() filtering from asyncTrimImmutableBuckets(). Would it be safer to skip CREATING buckets and retrigger or rescan for trimming once creation finishes, rather than holding the global trim/merge gate? If waiting is intentional, we should at least define and test a bounded-completion guarantee.
There was a problem hiding this comment.
Thanks, agreed this is important. The implementation keeps creation gating in the delete path and adds deterministic coverage to ensure we do not start deletion while create is blocked, then cleanup proceeds once create completes (testTrimDefersDeleteUntilSnapshotCreateCompletes). The trim/merge flow remains serialized by trimFuture intentionally so clear/trim observe a single ordered chain.
| verify(localStorage, atLeastOnce()).getBucketSnapshotSegment(anyLong(), anyLong(), anyLong()); | ||
| }); | ||
| } | ||
|
|
There was a problem hiding this comment.
The PR also addresses the snapshot-create/trim race condition mentioned in the motivation, though the new regression tests only cover snapshot-segment loading.
Could we add a deterministic trim test that:
- Blocks createBucketSnapshot()
- Triggers a trim while the bucket ID is still unavailable
- Verifies that deletion does not begin prematurely
- Then releases the snapshot creation and confirms that cleanup completes
It would also be useful to test revalidation when the bucket ceases to be orphaned while creation is pending. #26260 already includes deterministic coverage for both scenarios that could be adapted for this purpose.
There was a problem hiding this comment.
Added deterministic tests for the requested race scenarios in BucketDelayedDeliveryTrackerTest:
testTrimDefersDeleteUntilSnapshotCreateCompletestestTrimDeletesAfterCreateCompletesDespiteMarkDeleteChange
They block createBucketSnapshot(), trigger trim while bucket id is unavailable, verify no premature delete, then release create and assert cleanup behavior.
Motivation
Fix delayed-delivery bucket recovery and trim races that could stall progress or break trim cleanup:
Modifications
getScheduledMessages(), process snapshot-segment boundary transition before dropping an orphaned tail entry, while still suppressing delivery of that orphaned entry.ImmutableBucket.asyncDeleteBucketSnapshot():OptionalCompletableFuture(no synchronous throw)BucketDelayedDeliveryTrackerTest:testDoesNotLoadNextSnapshotSegmentBeforeCutofftestLoadsNextSnapshotSegmentAfterCutofftestTrimDefersDeleteUntilSnapshotCreateCompletestestTrimDeletesAfterCreateCompletesDespiteMarkDeleteChange