Skip to content

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

Open
jeqo wants to merge 1 commit into
mainfrom
jeqo/file-delete
Open

fix(inkless:storage): drain object-store deletes monotonically under throttling#715
jeqo wants to merge 1 commit 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 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. Because FileCleaner deletes 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-issued DeleteObjects for 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 of void, so callers can advance on partial progress.

  • S3 (S3Storage): single pass per 1000-key batch, collecting DeleteObjectsResponse.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.
  • Azure (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.
  • GCS (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.
  • In-memory / config-test backends: return the input set on success (contract propagation only).
  • FileCleaner: dereferences only the confirmed-deleted subset in the control plane and records that count.

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

Backoff

Backoff is layered rather than 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 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

  • FileCleanerMockedTest gains monotonic-drain, no-progress-backoff, and storage-failure cases; the existing cases stub the new return value.
  • New S3StorageDeleteTest (WireMock) covers single-pass partial return without retry, and a whole-request 503 that must not propagate.
  • Existing Azure and GCS integration tests (BaseStorageTest.testDeletes, run against Azurite / fake-gcs) exercise the updated backends.

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.

…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.

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

2 participants