fix(storage): close the upload handle we opened, and only that one - #1607
Open
grdsdev wants to merge 1 commit into
Open
fix(storage): close the upload handle we opened, and only that one#1607grdsdev wants to merge 1 commit into
grdsdev wants to merge 1 commit into
Conversation
`_request` closed the uploaded file handle after the `except HTTPStatusError` block, so the close only ran when the request succeeded. Any non-2xx raised `StorageApiError` straight past it and the handle stayed open. Since the handle only ever existed inside the local `files` mapping, the caller had no way to close it either. Python reports it as `ResourceWarning: unclosed file`. The close also lived in the wrong place. `_request` sees only a `BufferedReader` and cannot tell one that storage3 opened from one the caller passed in, so it closed both. A caller who supplies their own stream had it closed under them on the success path. Move the close to the two functions that open the file, `_upload_or_update` and `upload_to_signed_url`, which know whether they own the handle. A handle storage3 opened is closed on both paths; a caller-supplied stream is left alone on both paths. Supersedes #1575 and #1586, which both fixed the leak by wrapping `_request` in `try/finally`. That closes the leak but keeps the close in the frame that cannot tell the two cases apart, and extends the closing of caller-owned streams to the error path as well. Co-authored-by: Tushar Pagar <240662211+tushardev-365@users.noreply.github.com> Co-authored-by: chengwudi1 <chengwudi1@users.noreply.github.com>
Contributor
|
The following capabilities are marked
The following capabilities are marked
These may have been renamed, removed, or never registered. Please update the capability matrix. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Supersedes #1586 and the closed #1575.
The leak
_requestclosed the uploaded file handle after theexcept HTTPStatusErrorblock, so the close only ran on success. Any non-2xx raisedStorageApiErrorstraight past it and the handle stayed open. The handle only ever existed inside the localfilesmapping, so the caller had no way to close it either — Python reports it asResourceWarning: unclosed file. Bothupload()/update()(via_upload_or_update) andupload_to_signed_url()are affected.Credit for the diagnosis goes to @tushardev-365 in #1575, including the fd-count reproduction and the note that a retained traceback keeps the frame holding the reader alive.
Why not just
try/finallyaround_requestThat is what both #1575 and #1586 did, and it does close the leak. But it leaves the close in a frame that cannot tell the two cases apart:
_requestsees only aBufferedReader. A handle storage3 opened from a path and a stream the caller passed in are bothBufferedReader, so it closes both. Onmainthat already bites callers who supply their own stream — it is closed under them on the success path. Wrapping_requestintry/finallyextends that to the error path too, which breaks retry-after-failure:This change
Move the close into the two functions that open the file —
_upload_or_updateandupload_to_signed_url— which know whether they own the handle:_requestno longer touches the handle at all.Result: a handle storage3 opened is closed on both paths; a caller-supplied stream is left alone on both paths.
Tests
tests/_async/test_file_api.pyand its generated_synctwin, 4 tests each:test_upload_closes_file_handle_on_errortest_upload_closes_file_handle_on_success(regression)test_update_closes_file_handle_on_error—update()shares_upload_or_updatetest_caller_supplied_handle_is_left_open— asserts on both pathsThey assert on the real handle's
.closed(captured from thefilesmapping the client received) rather than patchingbuiltins.openand asserting on a mock. That approach is from #1575.Reverting only the source change turns 6 of the 8 red, including the caller-supplied guard on the success path, which fails on
maintoday.Note for reviewers
run-unasync.pycannot regenerate the existing storage test files: it emitsfrom unittest.mock import SyncMock(no such name) wherever a test usesAsyncMock, sotests/_sync/test_bucket.pyandtests/_sync/test_client.pyare hand-corrected and running the script today breaks them. The new test file avoidsAsyncMockentirely, so its_synccopy is genuinely generated and round-trips. Worth a separate issue.Verification
ruff checkandruff format --checkcleanmypyshows only the pre-existingpyicebergimport-not-found errors inanalytics.py, untouched here