Repository navigation
Stop appends, sync multipart copies, and commits at a failed part - #1183
Conversation
When an S3File write waits for one of the max_workers uploaded parts in flight, it now also checks the part copies of an append that have finished, and raises the error of a failed one, as it does for a failed uploaded part. The part copies are tracked in their own set, so they still do not count toward max_workers. S3FileSystem._finish_multipart_upload() now waits with FIRST_EXCEPTION before collecting the parts in part-number order, so that a failed part copy of a sync multipart copy is handled without waiting for the parts before it, and the parts queued after it are cancelled. Closes #1178 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The part copies of an append are now checked before each part is submitted, not only when the write waits for a part, so that a failed part copy stops the write with any max_workers. S3File.commit() waits with FIRST_EXCEPTION before collecting the parts, as _finish_multipart_upload() does, which now iterates the futures that the wait returns as done. The tests that replace wait() tell the waits apart by return_when, and the write-file helper sets _copies_in_flight. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| for part_number, data in writer.iter_parts( | ||
| buffer, first_part_number=len(self.multipart_upload_parts) + 1 | ||
| ): | ||
| if self._copies_in_flight: |
There was a problem hiding this comment.
Self-review round one (behavior and implementation)
- Base:
3a5018225e536ba5b0a920465e3f141dae0447e4, head:f135e16b99c518a08bd17a489e03c5fa8105c6c8(round one started at8cf11bf0; repairs are inf135e16b). - Covered:
S3File._upload_chunk(),_initiate_upload(),commit(), anddiscard();S3FileSystem._finish_multipart_upload()and its caller_copy_object_with_multipart_upload();AioS3File, which inherits all of them;- the tests that replace
wait(), the new tests, and the docs.
- Inputs:
/code-review(high) and a/simplifypass (reuse, simplification, efficiency, altitude). - Result: FINDINGS, repaired or deferred as below.
Repairs in f135e16b:
- The part copies were checked only inside the
max_workerswait branch, so an append that never filledmax_workers(defaultcpu_count() * 5) still uploaded all its data after a failed copy. They are now checked before each part is submitted, and before the wait, so a copy that has already failed does not wait on an upload either (this line). S3File.commit()collectedmultipart_upload_partsin part-number order. A failed later part, such as one of the many part copies of a large append, waited for the parts before it while the queued ones started. It now waits withFIRST_EXCEPTIONas_finish_multipart_upload()does._finish_multipart_upload()rescanned all futures behind anif pendingguard. It now iterates the futures returned as done, as the aio copy does.- The
done | copiedunion raised an arbitrary one of two failures. The two checks are now separate. - Tests:
_make_write_filedid not set_copies_in_flight;test_finish_multipart_upload_waits_for_running_partshad been weakened to its last call, and now asserts both waits;test_write_parts_in_flight/test_open_parts_in_flightnow steer and count onlyFIRST_COMPLETEDwaits, becausecommit()also waits now.
Deferred or rejected:
- A done callback per part, which would check without polling, was rejected. A callback bound to the file puts the file in a reference cycle, which delays fsspec's close-and-commit of a file dropped without
close()(measured for Stop a multipart write at a part found failed while it waits #1176). - The set comprehension costs O(running copies) per part. The set empties as the copies finish, and a copy part is up to 5 GiB, so the set is small. Not changed.
- Test assertions that overlap the neighbouring uploaded-part test were kept, for symmetry.
| parts being uploaded finish. A part that fails after the last wait raises its error | ||
| when the file is closed, or when the transaction commits, and the multipart upload is | ||
| aborted. | ||
| If one of these parts has failed when the write waits, or a part copied from the |
There was a problem hiding this comment.
Self-review round two (claims, callers, operations, evidence)
- Base:
3a5018225e536ba5b0a920465e3f141dae0447e4, head:f135e16b99c518a08bd17a489e03c5fa8105c6c8. - Scope: a full pass over the PR title and body, the commit messages, the changed docstrings and comments, this docs paragraph, and the premises of Failed part copies do not stop an append or a sync multipart copy #1178.
- Result: CLEAN after one PR-text correction.
Claims checked:
- "The check does not depend on the write waiting for a part." The copy check runs at the top of every iteration of
iter_parts(), outside themax_workersbranch.test_append_failed_part_copycoversmax_workers=4, where the second write never waits. - "Queued part copies are cancelled before they start; running ones are waited for before the abort." On the raise,
_finish_multipart_upload()andcommit()(throughdiscard()) cancel the futures they can and wait for the rest. Both new tests assert the cleanup started before part 1 finished and that part 3 was cancelled. - "
AioS3FileSystem._copy_object_with_multipart_upload()already stops at the first failure." It usesasyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)(pyathena/filesystem/s3_async.py:641). - "
AioS3Fileshares_upload_chunk(),_initiate_upload(), andcommit()."AioS3File(pyathena/filesystem/s3_async.py:1030) overrides none of them. - Docs: "before the write uploads another part" matches claim 1. "A part that fails later raises its error when the file is closed, or when the transaction commits" matches
commit(). Thecpparagraph ("A failed part copy aborts the multipart upload ... the part copies in flight finish first") is still accurate. No other text indocs/describes the order of part failures.
Existing callers and operations:
- Interrupts during the new waits go through the same
except BaseExceptioncleanup as before. - A future cancelled by
S3AioExecutor's loop shutdown is skipped by the new scans, and still raisesCancelledErrorfrom the in-order collection, as before. wait()over up to 10,000 futures adds one waiter per future, which is O(n) like the previousresult()loop.- Requests after a failed part copy drop from all the remaining parts or copies to the ones already running.
Evidence: local only, Python 3.13.1 on macOS.
- The full
tests/pyathena/filesystem/suite: 1221 passed, 9 skipped, with live S3, on the code off135e16b. - The 4 new test cases fail on
master. - The tests that replace
wait()passed 5 runs in a row. - AWS CI has not run (Draft).
Correction: the PR title said "commits at a failed part copy", but commit() stops at any failed part. It now reads "Stop appends, sync multipart copies, and commits at a failed part".
The part copy of test_append_failed_part_copy could fail before the first write checked the part copies, so that the first write raised instead of the second. The copy now waits for an event that the test sets after the first write. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
|
||
| def upload_part_copy(**kwargs): | ||
| # Copying until the first write has returned. | ||
| assert release.wait(5) |
There was a problem hiding this comment.
Independent review (relayed result, static review)
- Reviewer: Codex CLI 0.162.1, model
gpt-6-astra, reasoning efforthigh, run withcodex exec --sandbox read-only. Session01a13b27-261d-7221-87e4-9f3948d21b39. - Base:
3a5018225e536ba5b0a920465e3f141dae0447e4. Head:f135e16b99c518a08bd17a489e03c5fa8105c6c8. Reviewed in a detached worktree at that head. The snapshot and the PR worktree were unchanged afterwards. - Constraints:
- No edits, builds, tests, GitHub access, or network access.
- The prompt carried the literal diff and the intended behavior. It left out the PR number, PR text, commit messages, and prior findings.
- Coverage:
- the S3File lifecycle methods, the sync multipart copy and its finalization, both executors and their cancellation notification;
AioS3Fileinheritance, compared against the aio copy;- fsspec write/flush/close and transactions;
- part ordering, large part counts, cleanup, interrupts, the changed tests, and the docs.
- The reviewer noted that the patched
wait()helpers still tell upload backpressure apart from the commit and cleanup waits. - Result: FINDINGS, one finding.
P2: race in the append regression test. Location: tests/pyathena/filesystem/test_s3.py:7264 at f135e16b. The first write() submits the part copy in _initiate_upload(). The worker can raise PermissionError before that same write reaches the new copy check in _upload_chunk(). The first write then raises outside pytest.raises, and the test fails even though the implementation is correct. The later wait() does not prevent this. Suggested fix: hold the copy behind an event until the first write returns, then release it, and release it in cleanup too.
Verified against the code: the race is real. With max_workers=1, the worker starts the copy as soon as it is submitted. The test had passed only because of timing. The reviewer found no other actionable defects.
There was a problem hiding this comment.
Repair of the independent review finding (f135e16b -> cfd6d3b9 -> 50637290, test only)
cfd6d3b9: the part copy now waits for an event that the test sets after the firstwrite()returns, so the copy can no longer fail before that write checks the part copies.- Codex follow-up on
f135e16b..cfd6d3b9(session01a13b2e-b0eb-7c92-b6ac-c008d86808b1,gpt-6-astra, high, read-only) returned FINDINGS:- P2: the copy's
release.wait(5)would fail the first write after a 5-second stall. - P2: an assertion failure left the executor's threads running.
- P2: the copy's
50637290: the copy waits without a timeout. That is safe because afinallyblock always sets the event. The same block shuts down the executor.- Codex follow-up on
f135e16b..cfd6d3b9..50637290(session01a13b34-7e59-7ab0-a7c1-e22440ea597b, same settings): CLEAN. The race is gone formax_workers1 and 4. Cleanup releases and joins the workers. The test still fails without the copy check. The remaining 5-second timeout on the completionwait()cannot leak threads.
Self-review of the repair:
- Round one: the repair is limited to this test, and no implementation or shared helper changed.
- Round two: the PR body now describes the held copy and names
50637290as the tested commit.
Validation on 50637290:
- The test passed 10 times in a row.
- With the
pyathena/changes reverted, it fails (2 of 2) without hanging. just lintpassed.- The full
tests/pyathena/filesystem/suite passed: 1221 passed, 9 skipped, including live S3.
The part copy of test_append_failed_part_copy waited for its event with a 5-second timeout, whose expiry would fail the first write. The test sets the event in a finally block, so the copy now waits without a timeout, and the block also shuts down the executor when an assertion fails. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… part As S3File does since #1183: - A write of an AioS3StreamedFile raises a part copy of an append found failed before it submits a part, instead of uploading the rest of the data. - The commit raises a failed part without waiting for the parts before it, so that the parts that have not started are cancelled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WHAT
S3Filewrite submits a part, it now checks the part copies (UploadPartCopy) of an append that have finished, and raises the error of a failed one. The file is then closed without committing it and the upload is aborted, as Stop a multipart write at a part found failed while it waits #1176 does for a failed uploaded part. The part copies are tracked in their own set (_copies_in_flight), so they still do not count towardmax_workers, and the check does not depend on the write waiting for a part.S3FileSystem._finish_multipart_upload(), used by the multipart copy ofcp, now waits withreturn_when=FIRST_EXCEPTIONand raises a failed part from the futures returned as done, before collecting the parts in part-number order. A failed part copy is therefore raised without waiting for the part copies before it, and the part copies queued after it are cancelled before they start. The running ones are still waited for before the abort.S3File.commit(): waits the same way before collecting the parts, so a failed part, such as one of the many part copies of a large append, is handled without waiting for the parts before it.docs/filesystem.mdand the docstrings of_upload_chunk(),commit(), and_finish_multipart_upload()describe the new behavior.Behavior change for the release notes: in an append, the error of a failed part copy can now be raised by a
write(),flush(), orclose()that submits a part, instead of only byclose()/commit(). This matches the uploaded parts after #1176.WHY
Closes #1178.
_uploads_in_flight. The write uploaded all of the new data, andclose()orcommit()then raised the error and aborted the upload.S3File.commit(), collected the parts in part-number order. When a later part failed, the queued parts kept running until the loop reached it, and each one was a billed request.AioS3FileSystem._copy_object_with_multipart_upload()already stops at the first failure.A done callback on each part was not used for the append. A callback bound to the file would put the file in a reference cycle, so a file dropped without
close()would no longer be closed (and committed by fsspec's__del__) when its last reference goes away.TEST
Tested commit: 5063729 (Python 3.13.1, macOS).
just formatandjust lint: passed.mise exec -- markdownlint-cli2 docs/filesystem.md: 0 errors.uv run --env-file .env pytest -n 8 tests/pyathena/filesystem/: 1221 passed, 9 skipped, including the live S3 tests.master:TestS3File.test_append_failed_part_copy(max_workers1 and 4): the part copy of an append is held until the firstwrite()returns and has failed before the second, which raises with or without waiting for a part. Only one uploaded part was submitted, the upload is aborted once, and nothing is completed.TestS3FileSystem.test_finish_multipart_upload_stops_at_failed_partandTestS3File.test_commit_stops_at_failed_part: part 2 has failed while part 1 is still copying. The cleanup starts before part 1 finishes, part 3 is cancelled, and the upload is aborted.wait()did not expect the new wait:test_finish_multipart_upload_waits_for_running_partsnow asserts both waits.test_write_parts_in_flightandtest_open_parts_in_flightnow count and steer only theFIRST_COMPLETEDwaits._copies_in_flight.wait(), and the new tests, passed 5 times in a row;test_append_failed_part_copypassed 10 times in a row on 5063729.Not run:
AioS3FileSystemappend test.AioS3Fileshares_upload_chunk(),_initiate_upload(), andcommit(), and Stop a multipart write at a part found failed while it waits #1176 covered the wait with both executors.just test pyathenaas a whole, since no cursor or SQLAlchemy code changed.🤖 Generated with Claude Code