Hold the shared-handle test to the upload, and cover a failed read - #293
Merged
Merged
Conversation
Nothing ordered the two coroutines of the shared-handle test: on a machine where the writer finished before curl subscribed, the engine before the fix passed it. The writer now waits until curl reports bytes on the wire, bounded so that a build whose progress callback never reports still runs the writes. The second test uploads from a handle opened for writing, where every read fails. The failed request had nobody to release it; a debug build prints what is left behind, and the test reads that. The fix is php-src 909137229f9.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
The barrier counted scheduler turns, and an empty scheduler spins through ten thousand of them while curl is still connecting: the marker added with it fired on every run here, so the writes were landing before the upload as before. It waits five seconds by the clock now, and prints a line the expectation does not carry when it gives up, so a barrier that stops working cannot pass quietly. The leak test skips outside a debug build, which prints no leak report, and on Windows, where a file read is served synchronously and never reaches the branch under test.
The test rested on a read of a handle opened for writing failing. It fails here and succeeds on the CI runner, which answered "upload: sent" where the test expected "upload: failed", so the test says nothing there and broke the debug jobs everywhere. The leak the fix closes keeps the evidence it was found with: 528 bytes of request per read, reported by a debug build against libuv_reactor.c.
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.
Follows #292, from two critic passes over it. Every finding below was checked against the code before acting on it; where a path is read rather than run, it says so.
A failed read had nobody to release it.
io_file_read_cbnotifies with the request as the result and the request's own exception beside it (ext/asynclibuv_reactor.c), so a failed read reaches the branch that handles a failure of the event layer and never the one that inspects the request — which means the check added there in #292 was unreachable. The failure branch left the request behind, and the reactor hands an awaited request to its awaiter and nobody else. An upload from a handle opened for writing leaks 528 bytes of request per read: a debug build reports it againstlibuv_reactor.c(6072), with the exception object beside it. Released in that branch now, and the unreachable check is gone.tests/curl/071-read_error_releases_its_request.phptcovers it: the leak report goes to the output run-tests compares, so the test fails on a debug build that leaks. It skips on a release build, which prints no such report, and on Windows, where a file read is served synchronously and never reaches the branch under test.The shared-handle test did not force the overlap it tests, and the first barrier did not either. The writer did 100 writes through the thread pool while curl was still connecting, so on a machine where the writes finished before curl subscribed the engine before the fix would have passed. The barrier added for it counted scheduler turns — and the marker printed beside it fired on every run here: an empty scheduler spins through ten thousand turns while curl is still connecting, so the writes still landed before the upload. It waits five seconds by the clock now, and prints a line the expectation does not carry when it gives up, so a barrier that stops working cannot pass quietly.
Measured with the working barrier: SIGSEGV in 5 runs of 5 against the engine before #292, 5 green runs of 5 after.
Two paths closed from reading, neither reproduced by a script. The read callback answers PAUSE while a read of its own is in flight: libcurl asks for data again whenever the transfer is unpaused, and unpausing what is received resumes what is sent with it, so a second request could take the place of the first — which would then complete as a stranger to its own state, freed by nobody, its bytes missing from the body. And a state whose handle closed under it remembers the close: a handle closed with requests still in the thread pool notifies without an exception, this state waits for its own completion, and the request curl asks for next would otherwise reach the reactor for a read on a descriptor that is gone, raising inside the reactor's own callback. Two scripts written for these two paths did not reproduce either; they enforce the invariant the rest of the fix rests on.
The engine side is php-src
c63ad4096fa(true-async, merged intotrue-async-stable).Left out of this PR and filed as #294: a CURLFile upload keeps its read state on the mime callback argument, out of reach of
curl_async_event_stop, so itseventis never cleared and its subscription never dropped — the cancelled-state handling in #292 covers theCURLOPT_READDATAstate alone. Read from the code; a script for it did not reproduce either, and it wants its own reproduction before a fix. Beside it,CURL_READ_OWNS_FDis read twice and set nowhere, so bothclose(state->file.fd)branches are dead.