Skip to content

fix(inkless:storage): drain object-store deletes monotonically under throttling [KC-413] - #715

Merged
jeqo merged 2 commits into
mainfrom
jeqo/file-delete
Aug 13, 2026
Merged

fix(inkless:storage): drain object-store deletes monotonically under throttling [KC-413]#715
jeqo merged 2 commits into
mainfrom
jeqo/file-delete

Conversation

@jeqo

@jeqo jeqo commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

When object storage throttled deletes, the WAL file cleaner aborted the whole cycle and re-attempted 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. Since FileCleaner deletes from storage before updating the control plane, that throw dereferenced nothing for the whole batch — including keys S3 had already deleted in earlier slices. The next cycle re-issued DeleteObjects for objects that were already gone, so under sustained throttling the set never drained.

Change

ObjectDeleter.delete(Set) now returns the subset of keys confirmed deleted (already-absent counts as deleted) instead of void, so callers can advance on partial progress. FileCleaner dereferences only that subset; undeleted keys stay marked and are retried by the next periodic cycle.

  • S3: single pass per 1000-key batch, returning DeleteObjectsResponse.deleted() and logging the rest. No longer throws on partial or whole-request failure (including a 503 the SDK's adaptive retry exhausted).
  • Azure: deletes blob-by-blob, so it now accumulates the confirmed-deleted subset instead of throwing on the first failure and abandoning the rest.
  • GCS: stays all-or-nothing (documented inline) — Storage.delete(Iterable) reports batch failure as a thrown exception, so no confirmed subset can be extracted.
  • In-memory / config-test backends: return the input set on success.

No control-plane change needed: deleteFiles already accepts any subset of keys.

Observability

Partial progress is now the normal case, and nothing reported it: FileCleanerErrorRate counts thrown exceptions, which partial failure no longer raises, and FileCleanerFilesRate counts only what succeeded — so a cycle draining half its worklist looked identical to one draining all of it. New FileCleanerFilesFailedRate counts submitted minus confirmed; a sustained non-zero value means keys are being re-attempted every cycle rather than draining, which separates throttling (self-recovering) from a hard per-key failure.

Testing

  • FileCleanerMockedTest: partial drain, no progress, and storage failure, plus the failed-files count.
  • New S3StorageDeleteTest (WireMock): partial return without retry, and a whole-request 503 that must not propagate.
  • Azure/GCS covered by existing BaseStorageTest.testDeletes against Azurite / fake-gcs.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 FileCleaner to 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.

Comment thread storage/inkless/src/main/java/io/aiven/inkless/storage_backend/s3/S3Storage.java Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.

@jeqo
jeqo marked this pull request as ready for review July 22, 2026 18:30
@jeqo
jeqo force-pushed the jeqo/file-delete branch from 2cb651e to 4f13d6f Compare July 29, 2026 19:12
@jeqo
jeqo force-pushed the jeqo/file-delete branch from 4f13d6f to 714d732 Compare August 5, 2026 21:40
@jeqo
jeqo marked this pull request as draft August 6, 2026 21:26
@jeqo
jeqo force-pushed the jeqo/file-delete branch from 714d732 to 90d05e5 Compare August 11, 2026 20:51
@jeqo
jeqo changed the base branch from main to jeqo/kill-background-on-restart August 11, 2026 20:53
@jeqo
jeqo force-pushed the jeqo/file-delete branch from 90d05e5 to e78c7de Compare August 11, 2026 21:37
@jeqo
jeqo force-pushed the jeqo/file-delete branch from dd0373e to 894fa9c Compare August 11, 2026 22:17
@jeqo
jeqo changed the base branch from jeqo/kill-background-on-restart to main August 11, 2026 22:17
jeqo and others added 2 commits August 12, 2026 19:06
…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.

Request-rate backoff stays where it already lives: the S3 client's ADAPTIVE_V2
retry strategy within a cycle, and the file-cleaner schedule between cycles. No
in-run backoff is added here.

No control-plane change is needed: deleteFiles already accepts any subset.

Tests: FileCleanerMockedTest gains partial-drain, nothing-deleted, 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; Azure/GCS are covered by the
existing BaseStorageTest.testDeletes integration tests.
Partial delete progress is now the normal case: the delete contract returns the
subset of keys the backend confirmed, and a throttled or rejected key stays marked
for deletion instead of throwing. Nothing reports that. FileCleanerErrorRate counts
thrown exceptions, which the delete path no longer raises for partial failure, and
FileCleanerFilesRate counts only what succeeded - so a cycle that drains half its
worklist is indistinguishable from one that drains all of it, cycle after cycle.

Add FileCleanerFilesFailedRate, the difference between the keys submitted and the
keys confirmed. A sustained non-zero value is the signal that keys are being
re-attempted every cycle rather than draining, which is what distinguishes throttling
(recovers on its own) from a hard per-key failure (does not).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@jeqo
jeqo force-pushed the jeqo/file-delete branch from 894fa9c to e2b57cf Compare August 12, 2026 16:06
@jeqo
jeqo requested a balanced review from Copilot August 12, 2026 16:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Suppressed comments (2)

storage/inkless/src/main/java/io/aiven/inkless/storage_backend/azure/AzureBlobStorage.java:217

  • The existing BaseStorageTest.testDeletes only exercises an all-success call and discards the return value, so it does not cover this new partial-progress behavior. Add a focused test where one Azure deletion fails and a later one succeeds, asserting that the failed key is omitted and iteration continues; otherwise a regression back to aborting the batch would go undetected.
            } catch (final BlobStorageException e) {
                LOGGER.warn("Failed to delete {}; leaving it for the next cycle", key, e);
            } catch (final RuntimeException e) {
                LOGGER.warn("Failed to delete {}; leaving it for the next cycle", key, Exceptions.unwrap(e));

storage/inkless/src/main/java/io/aiven/inkless/storage_backend/gcs/GcsStorage.java:137

  • Storage.delete(Iterable) may have deleted some blobs before one batched result raises, so “on failure we delete nothing” is not guaranteed. The implementation safely retries the entire set because deletion is idempotent, but the comment should describe what is reported rather than claim no physical deletion occurred.
            // cannot extract a confirmed-deleted subset the way the S3 backend does. This stays
            // all-or-nothing: on success every key is gone (idempotent), and on failure we delete
            // nothing and let the FileCleaner cycle retry the whole set.

@jeqo
jeqo requested a review from tvainika August 13, 2026 07:57
@jeqo
jeqo marked this pull request as ready for review August 13, 2026 07:57
@jeqo jeqo changed the title fix(inkless:storage): drain object-store deletes monotonically under throttling fix(inkless:storage): drain object-store deletes monotonically under throttling [KC-413] Aug 13, 2026

@tvainika tvainika left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@jeqo
jeqo merged commit 83a351d into main Aug 13, 2026
11 checks passed
@jeqo
jeqo deleted the jeqo/file-delete branch August 13, 2026 09:48
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.

3 participants