Skip to content

fix(io): attempt every S3 delete and report how many failed - #966

Merged
wgtmac merged 11 commits into
apache:mainfrom
plusplusjiajia:fix-s3-delete-files-attempt-all
Oct 13, 2026
Merged

wgtmac merged 11 commits into
apache:mainfrom
plusplusjiajia:fix-s3-delete-files-attempt-all

Conversation

@plusplusjiajia

@plusplusjiajia plusplusjiajia commented Sep 26, 2026 •

Copy link
Copy Markdown
Member

Follow-up to #898 (comment).

ArrowS3FileIO::DeleteFiles returned at the first credential prefix whose delete failed, so files under the remaining prefixes were never attempted. It now attempts every file unless task submission fails, logs deletion failures, and returns Failed to delete N of M files. Exceptions thrown by individual deletes are counted too.

  • Deletion is sequential by default. InitializeS3FileIODeleteExecutor(Executor*) initializes a caller-owned executor shared by all S3 FileIOs, including registry-created instances. Initialize it before the first delete; otherwise, the first delete freezes the default nullptr. Repeating the same pointer is allowed, but replacing it is not. The executor must remain alive until the last delete completes.
  • s3.delete.num-threads limits worker tasks per call, defaulting to the hardware thread count. Workers share a file index, keeping task/future storage bounded by the configured parallelism. The shared executor controls total concurrency; this property does not create or resize a thread pool.
  • Accepted tasks finish before the call exits, including when submission returns an error or throws. Submission failures are returned without falling back to the calling thread and bypassing the executor's concurrency limit. All workers use the same delegate snapshot and the caller's logger.

Arrow has no public S3 batch-delete API, so each request still deletes one file. ResolvingFileIO keeps its existing fail-fast behavior across delegates.

Tests cover deletion across allowed/denied prefixes, thrown deletes, rejected/throwing submissions, bounded task counts and concurrency, immutable executor initialization, and executor configuration for both direct and registry-created FileIOs.

Comment thread src/iceberg/arrow/s3/arrow_s3_file_io.cc Outdated
Comment thread src/iceberg/arrow/s3/arrow_s3_file_io.cc Outdated
ArrowS3FileIO::DeleteFiles grouped locations by credential prefix and
returned at the first group that failed, so files in later groups were
never attempted. Java's S3FileIO attempts every batch, logs each failed
path and reports the failure count.

Delete each file through its delegate, log each failure, and return
"Failed to delete N of M files" at the end. This sends the same requests
as before, since Arrow's S3 DeleteFiles also deletes one file at a time.
ResolvingFileIO stays fail-fast across delegates, as it is in Java.
Java's S3FileIO runs its deletes on an executor sized by
s3.delete.num-threads, which defaults to the number of processors.
DeleteFiles now does the same, with the calling thread taking part.

Java also packs keys into DeleteObjects batches. Arrow has no public
batch delete for S3, so each thread still deletes one file at a time,
as Arrow's own DeleteFiles does.
std::jthread needs -fexperimental-library with libc++ 18 and 19, which
the Clang 18+ requirement covers, so use std::async futures instead;
they also wait for their thread if an exception unwinds.

Helper threads now bind the caller's logger, as logger.h prescribes for
thread pools. Without it their warnings went to the global logger while
the returned error only carries the failure count.
wait() leaves an exception stored in a helper's future, so a delete that
threw was neither counted nor reported, and DeleteFiles could return
success. get() rethrows it on the calling thread, as the sequential loop
did.
@plusplusjiajia
plusplusjiajia force-pushed the fix-s3-delete-files-attempt-all branch from 41ff77e to 492c6cb Compare September 29, 2026 04:11
Comment thread src/iceberg/arrow/s3/arrow_s3_file_io.cc Outdated
Comment thread src/iceberg/arrow/s3/arrow_s3_file_io.cc Outdated
Each DeleteFiles call started its own threads, so s3.delete.num-threads
bounded one call rather than the FileIO: concurrent calls could run
many times that number. The FileIO now owns an Arrow thread pool of
that size, created with the FileIO but starting workers on the first
delete, and runs each batch on it through TaskGroup, as Java's S3FileIO
does with its executor. Failures are still logged per file and
reported as a count.
Comment thread src/iceberg/arrow/s3/arrow_s3_file_io.cc Outdated
Comment thread src/iceberg/arrow/s3/arrow_s3_file_io.cc Outdated
Comment thread src/iceberg/arrow/s3/arrow_s3_file_io.cc Outdated
Comment thread src/iceberg/arrow/s3/arrow_s3_file_io.cc Outdated
Comment thread mkdocs/docs/file-io.md Outdated
@plusplusjiajia
plusplusjiajia force-pushed the fix-s3-delete-files-attempt-all branch from f319cff to 7db5868 Compare October 10, 2026 09:02
Use a shared caller-owned executor, defaulting to sequential deletion.
Bound tasks, count delete exceptions, and drain accepted work on failure.
@plusplusjiajia
plusplusjiajia force-pushed the fix-s3-delete-files-attempt-all branch from 7db5868 to e121314 Compare October 10, 2026 09:11
A rejected or throwing Submit returned its own error: with no worker
accepted, no file was attempted, and otherwise the accepted workers
still deleted every file while the failure count was lost.

Log the rejection and let the calling thread take the files no
accepted worker has, then report the count as usual. A rejected
worker's future is still waited for, since Submit may accept a task
before failing.

Also name the worker bound, forward-declare Executor in
arrow_io_util.h, and make the Executor docs say callbacks throughout.
@plusplusjiajia
plusplusjiajia force-pushed the fix-s3-delete-files-attempt-all branch from c79a6e9 to 4b042f8 Compare October 10, 2026 11:28

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

Thanks for your quick update!

I pushed a small follow-up: executor initialization is now immutable to make its lifetime contract clearer, and s3.delete.num-threads limits workers per call without creating a pool. (Sorry for my confusing comment which suggested to remove this flag exactly). BTW, submission failures are returned after draining accepted work, rather than falling back to the caller and bypassing the executor’s concurrency limit.

@wgtmac
wgtmac merged commit 7543e80 into apache:main Oct 13, 2026
15 checks passed
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.

4 participants