Skip to content

fix(inkless:storage): bound bulk-delete failure logging - #742

Draft
jeqo wants to merge 3 commits into
mainfrom
jeqo/bound-bulk-delete-logging
Draft

fix(inkless:storage): bound bulk-delete failure logging#742
jeqo wants to merge 3 commits into
mainfrom
jeqo/bound-bulk-delete-logging

Conversation

@jeqo

@jeqo jeqo commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

A delete pass that fails for every key it submitted logged one line per key (with a stack trace per key on Azure) and repeated it on every cleanup cycle, since undeleted keys stay marked for deletion. GCS still interpolated the whole key set into the exception message, the bug #733 fixed for S3.

Aggregate per-key failures into DeleteErrorSummary: a count by error code, at most 3 sampled hard failures, and the first hard cause. One line per call, bounded by the distinct error codes rather than the key count, at INFO when every failure was a throttle and WARN otherwise. GCS cannot report per-key results, so its message just carries the key count.

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
A delete pass that fails for every key it submitted logged one line per key
(with a stack trace per key on Azure) and repeated it on every cleanup cycle,
since undeleted keys stay marked for deletion. GCS still interpolated the whole
key set into the exception message, the bug #733 fixed for S3.

Aggregate per-key failures into DeleteErrorSummary: a count by error code, at
most 3 sampled hard failures, and the first hard cause. One line per call,
bounded by the distinct error codes rather than the key count, at INFO when
every failure was a throttle and WARN otherwise. GCS cannot report per-key
results, so its message just carries the key count.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jeqo
jeqo force-pushed the jeqo/bound-bulk-delete-logging branch from ffcad22 to 194bab8 Compare August 12, 2026 16:11
@jeqo
jeqo requested a balanced review from Copilot August 12, 2026 16:14

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

Bounds repeated bulk-delete failure logs across cloud storage backends.

Changes:

  • Adds aggregated error counts, samples, and causes.
  • Applies bounded logging to S3 and Azure.
  • Removes GCS key enumeration from exceptions.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
DeleteErrorSummaryTest.java Tests summary bounds and formatting.
DeleteErrorSummary.java Implements aggregated failure logging.
S3Storage.java Aggregates per-key S3 failures.
AzureBlobStorage.java Aggregates Azure delete exceptions.
GcsStorage.java Reports only failed key count.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +65 to +66
private static final Set<String> THROTTLE_ERROR_CODES =
Set.of("ServerBusy", "InternalError", "OperationTimedOut");
Comment on lines +77 to +82
if (samples.isEmpty()) {
logger.info(summary);
} else if (firstHardFailure != null) {
logger.warn(summary, firstHardFailure);
} else {
logger.warn(summary);
logDeleteErrors(response.errors());
collectDeleteErrors(response.errors(), errors);
}
errors.log(LOGGER, objectKeys.size(), deleted.size());
Base automatically changed from jeqo/file-delete to main 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.

2 participants