Repository navigation
Add a fan-out primitive to the S3 executors (GH-1332) - #1367
Conversation
S3Executor.map() and map_async() run a callable for each item in the executor and return the outcomes in input order. The calls check the stop of the batch before they start, and the first failure that counts is recorded in the thread of the failed call, so that no call starts after the failure that stops the batch. An interrupt or a cancellation of the waiting thread or task sets the stop, cancels the calls that have not started, waits for the running ones, and is re-raised; a second one stops that wait. submit_to() now cancels the submission when the caller's future is cancelled, so that a scope drops a waiting call at once. The range reads of both file classes, recursive chmod, rm, and clear_multipart_uploads of S3FileSystem, and the DeleteObjects requests and annotation copies of AioS3FileSystem use it. A failed or interrupted read, and a cancelled asyncio rm or multipart copy, now raise once their running requests have ended; an interrupt of rm or clear_multipart_uploads cancels the requests that have not started. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the /simplify review of the fan-out primitive: - S3FileSystem._map() and _map_async() own the one-shot scope of an operation and its shutdown without another wait, instead of five copies at the sites. - S3Executor.waiter() wraps a future for asyncio and retrieves its outcome, for wait_shielded(), the map, and the streamed drain; the asyncio map reuses its wrappers in the drain. - _S3WorkItem.link() owns both links between the caller's future and the submission, and drops the submission once the future has ended. - A map without stop_on_failure waits for all calls at once. - S3ClientError.code_of() returns the code of the ClientError that an error was raised from, for the InvalidRange and NoSuchUpload checks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A call that the executor failed or cancelled without running it, such as one whose thread could not start, settled its future without passing through the batch, so it neither stopped the batch nor became its failure: a parallel read then failed joining an exception, and chmod or clear_multipart_uploads ignored it. The waiting thread now records such outcomes, and a cancelled call's outcome is a CancelledError instead of raising from result(). Also: - release() sets the stop, also if a further interrupt stopped the drain before it did. - _map_async() lives on AioS3FileSystem, its only user. - The asyncio _delete_objects() returns at once without batches, as the synchronous one does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| self._record(e) | ||
| raise | ||
|
|
||
| def _record(self, error: BaseException | None) -> None: |
There was a problem hiding this comment.
Self-review round 1: implementation behavior (Claude Code; /simplify with four read-only agents, then /code-review at high effort)
Base 2a60ecc, head 4f90bd4 (round start) → 5ff74f6 (repairs).
Covered: S3Executor.map/map_async, _S3Batch (gate, latch, wait, drain, release), submit_to and _S3WorkItem, the seven sites in s3.py and s3_async.py, the docs, and the new and updated tests against B1–B6, S8, F2, F5, T1 and the amended I1.
FINDINGS, repaired:
- The executor's own failures were not counted (
s3_executor.py,_S3Batch.wait;/code-review). A call that the executor failed or cancelled without running it settled its future throughsettle(), not_call. It was neither latched nor did it stop the batch, so a parallel read failed inb"".joinwith aTypeError, andchmodorclear_multipart_uploadsignored it. A cancelled outcome maderesult()raiseCancelledError. Repair (5ff74f6): the waiting thread records the outcomes of the futures it sees end, and a cancelled outcome is aCancelledErrorvalue. Test:test_map_executor_rejection[failed|cancelled], which fails without the repair. - A second interrupt before
_stop()left the batch running (/code-review).release()now sets the stop in itsfinally. - Simplification and reuse (6590afd):
S3FileSystem._map()andAioS3FileSystem._map_async()own the one-shot scope and itsshutdown(wait=False), replacing five copies;S3Executor.waiter()replaces three copies of wrap-and-retrieve, and the asyncio drain reuses the map's wrappers;_S3WorkItem.link()owns both future links and drops the submission once done;- a single all-completed wait without
stop_on_failure; S3ClientError.code_of()for theInvalidRangeandNoSuchUploadchecks;_map_asyncmoved toAioS3FileSystem;- an empty asyncio delete returns early.
Deferred or rejected, with reasons:
- Removing
countsin favor of a wrappingfn(raised by two agents): rejected. It is in the posted design and is the hook for B5 in step 4. outcomestypedlist[T | BaseException], which needs the twocasts: deferred to step 4, where the result shape of the batch operations is decided.- A shared helper for the four "wait, recheck every second" loops: deferred. They differ in cancellation handling.
- Data that a second interrupt keeps in the frames of the drain (
/code-reviewRetry processing with API call #3): within the amended I1 and documented indocs/filesystem.mdas best effort. - O(n·k) re-waits with
countsandstop_on_failure=True: no caller yet; revisit in step 4. - A single completion signal per batch, and a shared test stub for copy plans: optional; skipped.
Validation at 5ff74f6:
just lintpassed.- The filesystem suite: 1473 passed, 6 skipped.
- Mutation check: 17 mutant/test pairs over 12 mutations, plus the new rejection test, all killed.
The bulk delete and the annotation copies of AioS3FileSystem no longer use asyncio.gather, and the synchronous filesystem runs its parallel requests in its request pool since #1359. The cancellation note covers the files of open_async(), whose reads are coroutines. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| | **File handles** | `S3File` with thread pool | `AioS3File` with thread pool; `AioS3StreamedFile` from `open_async()` | | ||
| | **Bulk delete** | Thread pool per batch | `asyncio.gather` per batch | | ||
| | **Multipart copy** | Thread pool per copy | Thread pool of the filesystem | | ||
| | **Bulk delete** | Request pool, `max_workers` requests at once | Request pool, `max_workers` requests at once | |
There was a problem hiding this comment.
Self-review round 2: claims, compatibility, operations (Claude Code)
Base 2a60ecc, head 5ff74f6 → 58ef05d (repairs to docs and a docstring).
Claims checked against the code and tests:
- PR body,
S3MapResult.failure"first counted failure in completion order": true for failures of calls; the executor's own failures are recorded when the waiting thread sees them (added to the body). - "The call that takes its place makes no request":
test_map_failure_stops_batchholds the waiter until the freed thread has taken the next call; the gate mutant fails it. - "
asyncio.run()shutdown waits for the running calls":test_map_async_at_loop_shutdown. The limit for a loop closed without cancelling is documented inmap_async's docstring. docs/filesystem.md, failed or interrupted reads "raised once they have ended", andrm/clear_multipart_uploadsinterrupts:test_failed_read_waits_for_running_readsandtest_interrupted_operation_cancels_queued_requests(real SIGINT; it fails on master with both requests sent).docs/aio.md, cancellation waits for "the range reads of a file": corrected. It holds foropen_async()files only. AnAioS3Fileread runs inasyncio.to_thread, whose cancellation does not wait for the thread.docs/aio.mdcomparison table: corrected. The bulk delete no longer usesasyncio.gather, and since Share one request pool per filesystem among its files and operations (GH-1332) #1359 the sync filesystem runs its parallel requests in its request pool, not per-call thread pools.AioS3FileSystemdocstring, "run parallel requests withasyncio.gather": corrected to per-object calls such as_mv()copies.
Existing callers:
S3File(executor=...)with a customS3Executorkeeps working:mapuses onlysubmit()throughsubmit_to()(test_map_executor_without_limit).- The new public names are
S3Executor.map,map_async,waiter, andS3MapResult; nothing is removed. submit_to()now cancels a cancelled caller's submission. Its callers (write sessions, the map, the asyncio copy) only gain the earlier release of queued arguments (S8).
AWS operator:
- An interrupt now stops the retries of a running DeleteObjects request; aborts keep
unstoppable_retries()(B4). _delete_batchstill invalidates the cache in itsfinally.- No request count changes on success; a failed or interrupted read now holds the caller until the running GETs end, which is bounded by their stopped retries.
Evidence: the local filesystem suite only, on a WSL2 host with real S3. CI and the full just test pyathena/sqla/sqla-async runs are pending before Ready.
From the independent review of the fan-out primitive: - A failure that the executor reports without running a call is now recorded by a done callback of its future, when it is set and before the setting thread takes another call, instead of by the waiting thread, which let later calls start and later failures win the latch. The batch drops its futures on every path, since the callbacks would otherwise form a cycle that keeps the outcomes until the cyclic garbage collector runs. - S3Executor.map() and docs/aio.md state that a pool must not map on itself; only scopes run such calls in the calling thread. - Tests: the completion-order tests wait for the recording of the preceding failure; the asyncio rm test checks the invalidation of each request while the other runs; the retrieval test fails the second call during the drain; the synchronous map tests run under call_bounded(); test_map_result_frees_outcomes checks the cycle. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| self._record(e) | ||
| raise | ||
|
|
||
| def _settled(self, future: Future[T]) -> None: |
There was a problem hiding this comment.
Independent review (relayed): Codex CLI (codex exec -s read-only), session 01a1b220-30fe-7313-a250-e7670c88b3a8. Static review only: no edits, execution, or network access.
Snapshot: detached worktree at 58ef05d. Base 2a60ecc. The package contained the literal diff, the specification and design comments, CLAUDE.md, docs/contributing.md, and docs/testing.md "Choose what to test". It contained no PR text or prior findings.
Covered: map/map_async and _S3Batch (the gate and latch, executor failures, wait, drain, release, F5), submit_to and link, every sync and asyncio site, pool and scope compatibility, the docs, and the added and rewritten tests.
FINDINGS (6), all verified and repaired in 81a0c9c:
- [P2] Executor-reported failures reached the latch too late (
_S3Batch.wait). Later calls could start, and a later failure could win the latch. Repair: a done callback on each future (_settled) records the failure when the future is set, before the setting thread takes another call.test_map_executor_rejectionnow also asserts that the next item does not run; it fails when the callback is removed.- Follow-on found while validating: the callbacks formed a cycle between the batch and its futures, which kept outcomes until the cyclic GC ran (
test_read_interrupted_at_each_opcode_frees_rangesfailed deterministically). Repair:batch.clear()in afinallyon every path. New test:test_map_result_frees_outcomes(GC disabled). It fails without the clear.
- Follow-on found while validating: the callbacks formed a cycle between the batch and its futures, which kept outcomes until the cyclic GC ran (
- [P2] The completion-order tests raced, because events were set before the failure was latched. Repair:
test_map_raises_first_failure_to_end,test_map_counts_failures, andtest_clear_multipart_uploads_raises_first_failurenow wait for the recording of the preceding failure (a pass-through patch of_S3Batch._record). - [P2] The rewritten asyncio rm test lost the per-request invalidation check. Repair: separate releases. The first object's cache is gone while the second request still runs and the task is not done.
- [P2] The retrieval test could pass without the callback. Repair: the second call fails only after the first stopped the batch, that is, during the drain. The waiting loop no longer retrieves anything, and the GC and handler checks span three loop passes.
- [P2] The sync map tests had no external bound. Repair: they run under the new
call_bounded()helper (a daemon thread with a join timeout). The opcode-injection test still runs inline, since its tracer is per thread. - [P3] The bare-pool self-map limit was undocumented. Repair: stated in
S3Executor.map()anddocs/aio.md.
Validation at 81a0c9c:
just lintpassed.- The filesystem suite: 1474 passed, 6 skipped.
- Mutation check: 18 mutant/test pairs, all killed.
The full tests/pyathena run at 58ef05d had 22 failures and 36 errors after the exported session credentials expired mid-run (overlapping runs). The erroring conformance classes pass on rerun (90 passed), and the full run will be repeated at the final head.
There was a problem hiding this comment.
Independent follow-up 1 (relayed): Codex CLI, session 01a1b253-771a-7380-9c91-5013f7848c6c. Static review of the repair range 58ef05d..81a0c9c at snapshot 81a0c9c.
FINDINGS (5), all verified and repaired in e0f0bfa:
- [P1] An error of
countsin a done callback escaped it and skipped the later callbacks. For a rejection reported by another thread, that included asyncio's, somap_asyncwaited forever. Repair:_recordcatches the predicate's error and records it as the failure. Test:test_map_async_counts_error_in_callback, which raises a non-ExceptionBaseException, since asyncio re-raisesSystemExitout of the loop. It fails when the guard is narrowed. - [P2] A future wakes its waiters before it runs its callbacks, so
result()could see a failure that was not yet recorded. Repair: the waiting thread also records the futures it sees end, andresult()records the finished ones before deciding.- Found while repairing: recording every future in
result()calledexception()on unfinished ones, which waits. A stopped batch would then have waited for its queued calls before cancelling them. Repair: only finished futures are recorded. New test:test_map_failure_cancels_queued_calls, whose executor never starts the held call; it fails without thedone()check.
- Found while repairing: recording every future in
- [P2] An interrupt entering the
finallythat cleared the batch kept the bodies in the traceback. Repair:result()clears the futures itself before returning, so theexcepthandler (release) covers every point.test_map_result_frees_outcomesfails without the clear. - [P2] The rejection test could pass with waiter-side recording. Repair: its executor runs the other calls in the submitting thread, so the call after the rejection starts before the map waits. It fails when the callback is removed.
- [P3] The opcode-injection test was unbounded. Repair: the whole
interrupt_each_opcoderuns undercall_bounded(120 s).
Also: test_map_counts_failures now orders its failures so that the first to end is not the last item. The latch mutant used to survive because result() re-records in input order.
Validation at e0f0bfa:
just lintpassed.- The filesystem suite: 1476 passed, 6 skipped.
- Mutation check: 20 mutant/test pairs plus the two new tests' mutants, all killed.
There was a problem hiding this comment.
Independent follow-up 2 (relayed): Codex CLI, session 01a1b27e-7f21-7722-946f-5b8018cd7d47. Static review of the repair range 81a0c9c..e0f0bfa at snapshot e0f0bfa.
FINDINGS (4), verified and repaired in 13860df:
- [P1] A cancellation that the executor reported after the wait began latched the failure, but did not wake a
FIRST_EXCEPTIONwait. A call held until cancelled then kept the map waiting forever. Repair: the waiting thread waits for one future (_ended). The done callbacks set it when every call has settled (a count under the batch lock), or_latchsets it when a failure stops the batch. The states are also rechecked every second. This also removes the repeated waits on every pending future that the efficiency review noted. Tests:test_map_deferred_cancellation_stops_batchandtest_map_async_deferred_cancellation_stops_batch. Both fail (bounded) when a stop does not end the wait. - [P2] A Ctrl-C in
counts, evaluated in the waiting thread, was swallowed. Repair: only the done callback (_settled) catches and latches the predicate's error;_recordlets it propagate. Test:test_map_interrupted_in_counts, which fails when_recordcatches. - [P2] The callback test depended on timing. Repair: the rejecting thread waits until the map has wrapped the future for asyncio. The test also asserts that the predicate's error does not escape into that thread, which is what the guard now protects, since the wait no longer depends on the skipped callbacks.
- [P2] Coverage gaps, both added:
test_map_records_failure_before_callbacks_run. AFuturewhose callbacks are deferred is patched into the module, soresult()'s own recording is the only path. It fails without it.- The map opcode-injection test now also asserts that outcomes are freed while the interrupt is held.
Found while validating: with the single wait, the reads of test_read_interrupted_twice_frees_ranges could end before the patched wait was entered. Once that was fixed, the test showed that a second interrupt in the drain's own wait keeps the futures in that wait's frame. Repair: release() also clears the frames of the propagating exception (sys.exception()). It fails without this. The interrupted-read tests now make their reads wait until the wait is entered.
Validation at 13860df:
just lintpassed.- The filesystem suite: 1480 passed, 6 skipped.
- Mutation check: 24 mutant/test pairs plus the release-frames mutant, all killed.
There was a problem hiding this comment.
Independent follow-up 3 (relayed): Codex CLI, session 01a1b2b6-1d16-7df3-bd0e-3ded3c232e04. Static review of the repair range e0f0bfa..13860df at snapshot 13860df.
FINDINGS (3):
-
[P1] An interrupt inside
_end(), in a callback that records a failure, left_endedpending, and the second_latchreturned early. A call held until cancelled then kept the map waiting. Repaired in 6b8f5f1:_latchsets the stop and ends the wait for every counted failure understop_on_failure, not only the first;- the waits also end once a failure has stopped the batch.
Test:
test_map_interrupted_while_ending_wait, which injects at each opcode of_endundercall_bounded. Without the repair it hangs until the bound. -
[P2] An interrupt caught by
_settledwhile a custom executor completes a call on the map's other thread. If the waiting thread releases before that callback returns, the callback's own executing frame, whichclear_framesskips, keeps the future and its body. Deferred, outside the amended I1. It needs a caller-supplied executor that runs callbacks in the interrupted main thread while the map waits in another thread. PyAthena's executors run the callbacks of calls in pool threads, and signals are delivered only to the main thread. Under the amendment, data retention under such interleavings is best effort. -
[P2] The deferred-cancellation tests used timers, and the callback test could check
escapedbefore the rejecting thread had returned. Repaired:- the cancellations happen once the map waits (a patched
waitand a watchedS3Executor.waiter); - the callback test waits for the rejecting thread to finish before asserting.
- the cancellations happen once the map waits (a patched
Validation at 6b8f5f1:
just lintpassed.- The filesystem suite: 1479 passed, 6 skipped. 4 fixture setup errors in
TestS3FileSystemGetpass on rerun (45 passed). - The new mutant was killed.
The independent review loop ends here. The initial review and three follow-ups found 6, 5, 4, and 3 findings. The last open item is the deferred interleaving above, which needs a caller-supplied executor and an interrupt inside its callback. The remaining validation is the full local AWS run at the final head and CI after Ready.
From the follow-up of the independent review: - An error of the counts predicate in a done callback is the failure of the map instead of escaping the callback, which skipped the callbacks after it, including the one that wakes an asyncio map. - A future wakes its waiters before it runs its callbacks, so the waiting thread also records the futures that it sees end; result() records only finished futures, since exception() waits for one that has not ended, which would have kept a stopped batch waiting for its queued calls. - result() drops the futures itself, so that an interrupt after it returns finds none kept by the batch, instead of a finally after it. - Tests: the rejection test runs the other calls in the submitting thread, so the call after the rejection starts before the map waits; test_map_failure_cancels_queued_calls and test_map_async_counts_error_in_callback are new; the counts test orders its failures so that the first to end is not the last item; the opcode injection test is bounded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
From the second follow-up of the independent review: - The waiting thread waits for one future, which the done callbacks of the calls set when every call has settled or when a failure stops the batch, with the states checked every second. A cancellation that the executor reported after the wait began did not wake a wait for the first exception, so a call held until it was cancelled kept the map waiting forever. It also replaces the repeated waits on every pending future. - Only the done callback catches an error of the counts predicate; one in the waiting thread, such as an interrupt, is raised. - release() also clears the frames of an interrupt that stopped the drain, whose traceback keeps the futures of the drain's wait. - Tests: deferred cancellation (sync and asyncio), an interrupt in the predicate, a failure seen before its callback runs, the predicate's error not escaping into the thread that sets the future, and retention in the opcode injection over map(). The interrupted-read tests make their reads wait until the wait is entered, since the wait is skipped when every call has ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
) From the third follow-up of the independent review: - A later failure, or the same one recorded again by the callback that caught an interrupt of the signal, sets the stop and ends the wait again, and the wait also ends once a failure has stopped the batch, so that an interrupt in the signal does not keep the map waiting for a call held until it is cancelled. - Tests: the deferred cancellations happen once the map waits, not after a timer; the callback test checks the rejecting thread once its callbacks have returned; test_map_interrupted_while_ending_wait injects an interrupt at each opcode of the signal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WHAT
Step 3 of #1332: one fan-out primitive for the parallel leaf requests of the S3 filesystems, as designed in the step 3 comment.
S3Executor.map(fn, items, *, stop_on_failure=True, counts=None)andmap_async()return anS3MapResult(outcomes in input order, andfailure, the first counted failure in completion order). They are concrete methods ofS3Executor, built onsubmit_to(), so any executor works, including one given toS3File(executor=)with onlysubmit()andshutdown(). Every item is submitted at once; the executor applies its own limit.countsin a done callback becomes the failure instead of escaping the callback.stop_on_failure, the first counted failure cancels the queued calls, stops the retries of the running ones, and is raised once they have ended (B2).stop_on_failureis, setting the stop first, and is re-raised (B3). A second one stops the wait; the running calls finish in the executor (amended I1).map_asyncdrains in its own task, so the shutdown ofasyncio.run(), which cancels the task, also waits for the running calls (B6). A loop closed without cancelling its tasks is a documented limit.submit_to()cancels the submission when the caller's future is cancelled, so that a scope drops a waiting call at once (S8);_S3WorkItem.link()owns both links.S3Executor.waiter()wraps a future for asyncio and retrieves its outcome (shared bywait_shielded(), the map, and the streamed drain).S3ClientError.code_of()returns the code of theClientErroran error was raised from.Sites moved to the primitive:
S3File._fetch_rangeandAioS3StreamedFile._fetch_range;S3FileSystem.chmod;S3FileSystem._delete_objects(rm) andAioS3FileSystem._delete_objects(_rm), withstop_on_failure=False;S3FileSystem.clear_multipart_uploads, withstop_on_failure=FalseandcountsexcludingNoSuchUpload;The operations run through
S3FileSystem._map()andAioS3FileSystem._map_async(), which own the one-shot scope and shut it down withwait=False, so that a second interrupt is not followed by another wait in__exit__.Changes that callers observe:
S3FileandAioS3StreamedFileparallel readsrm,clear_multipart_uploadsAioS3FileSystem._rm()Docs:
docs/filesystem.md(interrupts of parallel reads,rm,clear_multipart_uploads, recursivechmod) anddocs/aio.md(cancellation of the fan-outs,S3Executor.map(), and the comparison table, whoseasyncio.gatherand per-call thread pool entries were obsolete).WHY
#1332 step 3: the track-before-schedule, cancel-before-stop, and frame-clearing rules were written by hand at each site, with different failure semantics.
rmandclear_multipart_uploadswaited for every request on an interrupt, and the asyncio fan-outs returned on a cancellation while their requests ran (B6 gap). Step 4 (#1322 batch operations, #1344) builds on this primitive and itscountshook (B5).Out of scope, per the design: the finalizer of an unclosed multipart file (#1364); the cooperative stop of writers in B2 (step 4);
rm(batch_size=)and per-call task threads (step 4);_S3WriteSessionandReadAheadStream, which own their tracking.TEST
Tested commit: 6b8f5f1.
just lint: passed.Full local suite (
uv run --with 'botocore[crt]' --env-file .env pytest -n 8 tests/pyathena, real AWS): 4089 passed, 15 skipped.Filesystem suite at the same head: 1479 passed and 6 skipped, with 4 fixture setup errors in
TestS3FileSystemGetthat pass on rerun (45 passed). The full run above passed them.New tests:
TestS3Executor:test_maptest_map_executor_without_limittest_map_executor_rejection[failed|cancelled]test_map_failure_cancels_queued_callstest_map_deferred_cancellation_stops_batch(and the asyncio variant)test_map_records_failure_before_callbacks_runtest_map_interrupted_in_countstest_map_interrupted_while_ending_waittest_map_result_frees_outcomestest_map_async_counts_error_in_callbacktest_map_failure_stops_batch(with a delayed waiter, so the freed place is taken first)test_map_raises_first_failure_to_endtest_map_counts_failurestest_map_interrupted_without_stop_on_failuretest_map_interrupted_twicetest_map_failure_frees_outcomestest_map_interrupted_at_each_opcodetest_map_async_cancelledtest_map_async_failures_are_retrievedtest_map_async_at_loop_shutdowntest_submit_to_cancel_frees_waiting_calltest_s3.py:test_interrupted_operation_cancels_queued_requests[rm|clear_multipart_uploads](real SIGINT)test_clear_multipart_uploads_raises_first_failuretest_failed_read_waits_for_running_readstest_s3_async.py:test_copy_object_with_multipart_upload_cancelled_annotation_copytest_close_after_interrupted_cancel: the file-level test of the drain's one-second recheck, deferred from Share one request pool per filesystem among its files and operations (GH-1332) #1359.Updated tests:
test_rm_cancel_invalidates_cache_after_each_requestbecametest_rm_cancel_waits_for_running_requests.test_close_waits_for_cancelled_readnow cancels the read twice, since one cancellation now drains.test_cancelled_read_frees_rangesno longer depends on the old per-range tasks.test_submit_to_cancelled_executor_failurestarts the submission before cancelling.S3Executor.map.pyathena.filesystem.s3.waitmoved tos3_executor.wait. The interrupted-read tests make their reads wait until the wait is entered, since the wait is skipped when every call has ended.test_interrupted_read_frees_rangespatches with a plain function, since aMagicMockkeeps its arguments.Mutation check: each new test fails under its mutation. The mutations were:
countsignored;submit_to();rmwithstop_on_failure.The site tests fail on master's source. The exception is
test_clear_multipart_uploads_raises_first_failure, which guards existing precedence and fails under thecountsmutation.Reviews:
Not run locally:
just test sqlaandjust test sqla-async. This change does not touch the dialect, and CI runs them after Ready.🤖 Generated with Claude Code