fix(inkless:storage): drain object-store deletes monotonically under throttling#715
Open
jeqo wants to merge 1 commit into
Open
fix(inkless:storage): drain object-store deletes monotonically under throttling#715jeqo wants to merge 1 commit into
jeqo wants to merge 1 commit into
Conversation
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates Inkless storage deletion semantics to support monotonic draining under object-store throttling: storage backends now report which keys were confirmed deleted so FileCleaner can advance control-plane dereferences on partial progress instead of reissuing redundant deletes.
Changes:
- Change
ObjectDeleter.delete(Set)to return the subset of keys confirmed deleted (idempotent: already-absent counts as deleted). - Update
FileCleanerto dereference only confirmed-deleted keys and apply backoff when a cycle drains nothing. - Update S3/Azure/In-memory/test backends to implement partial-progress deletion behavior and add/adjust tests for the new contract and throttling scenarios.
Reviewed changes
Copilot reviewed 9 out of 9 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| storage/inkless/src/main/java/io/aiven/inkless/storage_backend/common/ObjectDeleter.java | Changes the multi-key delete contract to return confirmed-deleted keys. |
| storage/inkless/src/main/java/io/aiven/inkless/delete/FileCleaner.java | Uses partial deletion results to dereference only confirmed-deleted keys; backs off when no progress is made. |
| storage/inkless/src/main/java/io/aiven/inkless/storage_backend/s3/S3Storage.java | Implements single-pass S3 batch delete that returns confirmed deletions and logs per-key/whole-request failures without aborting progress. |
| storage/inkless/src/main/java/io/aiven/inkless/storage_backend/azure/AzureBlobStorage.java | Accumulates and returns per-blob confirmed deletions instead of aborting on first failure. |
| storage/inkless/src/main/java/io/aiven/inkless/storage_backend/gcs/GcsStorage.java | Propagates new return type while keeping batch delete effectively all-or-nothing on thrown failures. |
| storage/inkless/src/main/java/io/aiven/inkless/storage_backend/in_memory/InMemoryStorage.java | Propagates new return type (returns input set on success). |
| storage/inkless/src/test/java/io/aiven/inkless/config/ConfigTestStorageBackend.java | Propagates new return type for config-test backend. |
| storage/inkless/src/test/java/io/aiven/inkless/delete/FileCleanerMockedTest.java | Adds tests for monotonic draining, no-progress backoff, and storage-failure handling; updates stubs for new return type. |
| storage/inkless/src/test/java/io/aiven/inkless/storage_backend/s3/integration/S3StorageDeleteTest.java | Adds WireMock integration coverage for partial deletions and whole-request failure behavior. |
…throttling Under S3 throttling the FileCleaner cycle aborted entirely: S3Storage.delete threw on the first per-key or whole-request error, so FileCleaner never dereferenced the files it had already deleted. The next cycle re-fetched the same set and re-issued DeleteObjects for objects that were already gone, looping without draining and amplifying the throttle. Change the delete contract so partial progress is preserved: - ObjectDeleter.delete(Set) now returns the subset of keys confirmed deleted (idempotent: already-absent keys count as deleted) instead of void. The in-memory and config-test backends return the input set on success. - S3Storage.delete does a single pass per 1000-key batch: it collects the keys S3 confirmed deleted (DeleteObjectsResponse.deleted()) and logs the rest, distinguishing throttling from hard errors for diagnostics only. It no longer throws for partial or whole-request failures (incl. a 503 the SDK's adaptive retry exhausted); undeleted keys stay marked for deletion and are retried on the next cycle. - AzureBlobStorage.delete deletes one blob at a time, so it now accumulates and returns the confirmed-deleted subset instead of throwing on the first failure and abandoning the rest (same monotonic-drain benefit as S3). - GcsStorage.delete stays all-or-nothing: Storage.delete(Iterable) is an atomic batch that raises a genuine failure as a thrown exception rather than a per-blob flag, so a confirmed-deleted subset cannot be extracted; a thrown batch drains nothing and the whole set is retried next cycle (documented inline). - FileCleaner dereferences only the confirmed-deleted subset in the control plane and records that count, so the set shrinks monotonically instead of re-attempting already-deleted keys. Backoff is layered, not duplicated inside the delete call: the S3 client's ADAPTIVE_V2 retry strategy handles request-rate throttling within a cycle, and FileCleaner now applies its exponential backoff whenever a cycle drains nothing (previously only on a thrown exception, which throttling no longer produces), so sustained throttling no longer retries at a fixed cadence. No control-plane change is needed: deleteFiles already accepts any subset. Tests: FileCleanerMockedTest gains monotonic-drain, no-progress-backoff, and storage-failure cases (and the existing cases stub the new return value); new S3StorageDeleteTest covers single-pass partial return without retry and a whole-request failure that must not propagate; existing Azure/GCS integration tests (BaseStorageTest.testDeletes) cover the updated backends.
jeqo
marked this pull request as ready for review
July 22, 2026 18:30
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.
When object storage throttled deletes, the WAL file cleaner would abort the whole cycle and re-attempt keys it had already deleted, looping without draining and amplifying the throttle. This makes deletion drain monotonically: only the keys the backend confirms deleted are dereferenced, and the rest are retried on the next cycle.
Problem
S3Storage.delete(Set)threw on the first per-key or whole-request error. BecauseFileCleanerdeletes from storage before updating the control plane, that throw meant no files were dereferenced for the whole batch — including keys that S3 had already deleted in earlier slices. The next cleaner cycle re-fetched the same set and re-issuedDeleteObjectsfor objects that were already gone, so under sustained throttling the set never drained and the redundant requests amplified the throttle.Change
The
ObjectDeleter.delete(Set)contract now returns the subset of keys confirmed deleted (idempotent: already-absent keys count as deleted) instead ofvoid, so callers can advance on partial progress.S3Storage): single pass per 1000-key batch, collectingDeleteObjectsResponse.deleted()and logging the rest (throttle vs hard error, for diagnostics only). It no longer throws for partial or whole-request failures (including a 503 the SDK's adaptive retry exhausted); undeleted keys stay marked for deletion and are retried next cycle.AzureBlobStorage): deletes one blob at a time, so it now accumulates and returns the confirmed-deleted subset instead of throwing on the first failure and abandoning the rest — same monotonic-drain benefit as S3.GcsStorage): stays all-or-nothing (documented inline).Storage.delete(Iterable)is an atomic batch that raises a genuine failure as a thrown exception rather than a per-blob flag, so a confirmed-deleted subset cannot be extracted; a thrown batch drains nothing and the whole set is retried next cycle.FileCleaner: dereferences only the confirmed-deleted subset in the control plane and records that count.No control-plane change is needed —
deleteFilesalready accepts any subset of keys.Backoff
Backoff is layered rather than duplicated inside the delete call: the S3 client's
ADAPTIVE_V2retry strategy handles request-rate throttling within a cycle, andFileCleanernow applies its existing exponential backoff whenever a cycle drains nothing (previously only on a thrown exception, which throttling no longer produces). So sustained throttling no longer retries at a fixed cadence. There is deliberately no bespoke in-call retry in the backend, which would fight the adaptive limiter and block the cleaner thread.Testing
FileCleanerMockedTestgains monotonic-drain, no-progress-backoff, and storage-failure cases; the existing cases stub the new return value.S3StorageDeleteTest(WireMock) covers single-pass partial return without retry, and a whole-request 503 that must not propagate.BaseStorageTest.testDeletes, run against Azurite / fake-gcs) exercise the updated backends.