Repository navigation
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4405 +/- ##
==========================================
+ Coverage 86.69% 86.87% +0.19%
==========================================
Files 527 527
Lines 20902 20957 +55
==========================================
+ Hits 18119 18205 +86
+ Misses 2783 2752 -31
🚀 New features to boost your workflow:
|
dd5df05 to
44b53fe
Compare
|
Where the five uncovered patch lines are, measured with the same One is the guard this change is about. One is a null easy handle in Three are the I would rather say that plainly than add cases that pin defensive branches into place with test-only seams. If you want any of the three covered, say which and I will send them. |
44b53fe to
e625eef
Compare
…_read Two corrections to the change before it, both found reviewing it rather than reported. The wait cannot be interrupted. wakeupBackgroundThread reaches the worker through the multi handle, and the whole point of that branch is that there is not one, so the destructor cannot cut the wait short and the previous commit saying teardown was unchanged is wrong. Destroying a client whose worker was inside the wait measured 168 ms, five runs out of five. Taking the same wait in 16 ms slices and rechecking is_shutdown_ leaves the retry rate where it was and brings that to 10 ms, five runs out of five. And curl_multi_info_read was still called with the handle libcurl had refused to give. curl_multi_poll and curl_multi_wait cannot see one, since they sit inside the branch that only runs when perform succeeded. curl_multi_add_handle and curl_multi_remove_handle can, but they are in doAddSessions and doRemoveSessions, which open-telemetry#4395 and open-telemetry#4405 rewrite, so they are named in the description rather than changed here. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
5a7e671 to
707ce77
Compare
5ef4bf6 to
1e12bd8
Compare
…_read Two corrections to the change before it, both found reviewing it rather than reported. The wait cannot be interrupted. wakeupBackgroundThread reaches the worker through the multi handle, and the whole point of that branch is that there is not one, so the destructor cannot cut the wait short and the previous commit saying teardown was unchanged is wrong. Destroying a client whose worker was inside the wait measured 168 ms, five runs out of five. Taking the same wait in 16 ms slices and rechecking is_shutdown_ leaves the retry rate where it was and brings that to 10 ms, five runs out of five. And curl_multi_info_read was still called with the handle libcurl had refused to give. curl_multi_poll and curl_multi_wait cannot see one, since they sit inside the branch that only runs when perform succeeded. curl_multi_add_handle and curl_multi_remove_handle can, but they are in doAddSessions and doRemoveSessions, which open-telemetry#4395 and open-telemetry#4405 rewrite, so they are named in the description rather than changed here. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
a2f59a9 to
7714cb9
Compare
…_read Two corrections to the change before it, both found reviewing it rather than reported. The wait cannot be interrupted. wakeupBackgroundThread reaches the worker through the multi handle, and the whole point of that branch is that there is not one, so the destructor cannot cut the wait short and the previous commit saying teardown was unchanged is wrong. Destroying a client whose worker was inside the wait measured 168 ms, five runs out of five. Taking the same wait in 16 ms slices and rechecking is_shutdown_ leaves the retry rate where it was and brings that to 10 ms, five runs out of five. And curl_multi_info_read was still called with the handle libcurl had refused to give. curl_multi_poll and curl_multi_wait cannot see one, since they sit inside the branch that only runs when perform succeeded. curl_multi_add_handle and curl_multi_remove_handle can, but they are in doAddSessions and doRemoveSessions, which open-telemetry#4395 and open-telemetry#4405 rewrite, so they are named in the description rather than changed here. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…_read Two corrections to the change before it, both found reviewing it rather than reported. The wait cannot be interrupted. wakeupBackgroundThread reaches the worker through the multi handle, and the whole point of that branch is that there is not one, so the destructor cannot cut the wait short and the previous commit saying teardown was unchanged is wrong. Destroying a client whose worker was inside the wait measured 168 ms, five runs out of five. Taking the same wait in 16 ms slices and rechecking is_shutdown_ leaves the retry rate where it was and brings that to 10 ms, five runs out of five. And curl_multi_info_read was still called with the handle libcurl had refused to give. curl_multi_poll and curl_multi_wait cannot see one, since they sit inside the branch that only runs when perform succeeded. curl_multi_add_handle and curl_multi_remove_handle can, but they are in doAddSessions and doRemoveSessions, which open-telemetry#4395 and open-telemetry#4405 rewrite, so they are named in the description rather than changed here. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
f4a41eb to
18bb79d
Compare
7974443 to
31e0e9b
Compare
…_read Two corrections to the change before it, both found reviewing it rather than reported. The wait cannot be interrupted. wakeupBackgroundThread reaches the worker through the multi handle, and the whole point of that branch is that there is not one, so the destructor cannot cut the wait short and the previous commit saying teardown was unchanged is wrong. Destroying a client whose worker was inside the wait measured 168 ms, five runs out of five. Taking the same wait in 16 ms slices and rechecking is_shutdown_ leaves the retry rate where it was and brings that to 10 ms, five runs out of five. And curl_multi_info_read was still called with the handle libcurl had refused to give. curl_multi_poll and curl_multi_wait cannot see one, since they sit inside the branch that only runs when perform succeeded. curl_multi_add_handle and curl_multi_remove_handle can, but they are in doAddSessions and doRemoveSessions, which open-telemetry#4395 and open-telemetry#4405 rewrite, so they are named in the description rather than changed here. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Teardown touched the easy handle in the wrong order and on the wrong thread. Cleanup cleared CURLOPT_PRIVATE and called curl_easy_reset on it, from whichever thread ran the cancel, while the transfer could still be active. doRemoveSessions then freed the header list before curl_multi_remove_handle and cleaned the handle up whatever that call returned. libcurl is explicit on all three: changing options while a transfer is in progress may have undefined behaviour, the header list has to outlive the handle that points at it, and a handle has to leave the multi handle before it can be cleaned up. Cleanup now hands the resource over untouched. doRemoveSessions removes it, checks the CURLMcode, and only then frees the list and the handle. A failed removal leaves both alone: leaking one easy handle is better than freeing one the multi stack may still own. A handle that was never added reports CURLM_OK, so no separate bookkeeping is needed to tell the two apart. Clearing CURLOPT_PRIVATE was load bearing rather than tidy up. The IO loop reads it back to decide whether a CURLMSG_DONE belongs to a session that has already gone, so PerformCurlMessage checks is_cleaned_ instead. That keeps the behaviour without writing to a handle from a thread that does not own it. Handing the handle over untouched also leaves CURLOPT_PRIVATE and the write, header, read and progress callback data pointing at the session and its operation, so the owner has to outlive the handle. On the abort path it did not. ScheduleAbortSession takes the session out of sessions_, so doRemoveSessions cannot find it to hold one, and doAbortSessions kept the only remaining shared_ptr in a local map that died on return. The handle could reach curl_multi_remove_handle and curl_easy_cleanup with those pointers dangling, and curl_easy_cleanup is documented as able to run the progress and header callbacks. pending_to_remove_sessions_ already exists for this: doRemoveSessions swaps it out and holds it until the handle is freed, and the abort path hands the session to it. Part of open-telemetry#4391. The retry path still calls curl_multi_remove_handle and curl_multi_add_handle without checking either result, so the issue stays open. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
1900e81 to
aac21dc
Compare
The 1.30.0 release rewrote the Unreleased section, which is the whole conflict. Both sides kept, ours after main's. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The 1.30.0 release rewrote that section, and resolving the conflict in place left this entry inside the released block, where it reads as shipped. Rebuilt as main's file with this branch's entry inserted under Unreleased, so the only difference from main is the entry itself. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
Fixes #4391.
What was wrong, and one thing I had wrong about it
The issue has three parts:
Cleanup()writes to an easy handle that may still be transferring,doRemoveSessions()frees the header list before it takes the handle out of the multi handle, and neither the removal nor the retry path reads theCURLMcodeit gets back. The first two are ordering and they're fixed by moving two calls.The third one I got wrong in my own comment on the issue, where I said a handle the multi handle never took reports
CURLM_OK, so nothing has to remember which handles were added. That's true of the libcurl I tested on and it isn't true in general. The prologue ofcurl_multi_remove_handlepicked up a second condition for two minor releases:if(!GOOD_EASY_HANDLE(data))CURLM_OKif(!GOOD_EASY_HANDLE(data) || !multi->num_easy)CURLM_BAD_EASY_HANDLEif(!GOOD_EASY_HANDLE(data))CURLM_OKfind_package(CURL)here has no lower bound, so both are supported. And the version window isn't even needed to reach the same branch: I measuredcurl_multi_remove_handle(NULL, easy)returningCURLM_BAD_HANDLEon 8.14.1, which is what a client whosecurl_multi_init()failed hands to every removal.What made that reading dangerous is what the failure branch did with it.
pending_to_remove_session_handles_is swapped into a local, so acontinuepast a record ends the life of that record at the end of the pass. The easy handle and its header list go with it, and so does theshared_ptr<Session>the same pass was releasing, while the handle still carries that session inCURLOPT_PRIVATE. The message loop reads it straight back:What this does now
curl_multi_add_handleaccepts it, and unrecorded whencurl_multi_remove_handlegives it back. A handle that was never accepted is freed without asking the multi handle anything, so neither libcurl's answer nor the version it came from decides the path.curl_multi_cleanuphas retired the multi they were on, inresetMultiHandle()and in the destructor. Cleanup is not what detaches them:multi.his explicit that it does not free or touch any individual easy handle. What makes the order safe is that the multi is gone by the time they are freed. No retry loop, because a refusal here doesn't become acceptance later, and I didn't want a busy path in the IO loop.doRetrySessions()goes through the same accounting and reports a re-arm libcurl won't accept, which is the silence the issue asks about.Evidence
Fail before, pass after, against this branch with the change reverted and the two scenarios rewritten against the members that existed before it. The commit I ran that on has been rebased away since, so reproducing it means doing the revert rather than fetching a sha:
ARefusedRemovalKeepsTheHandleAndTheSessionItNamesASecondHandleFromTheSameSessionDoesNotDisplaceTheFirstEach case was then checked against the mutation it's meant to catch, on an otherwise clean tree:
ARefusedRemovalKeepsTheHandleAndTheSessionItNamesAHandleTheMultiHandleNeverTookIsFreedNotStrandedASecondHandleFromTheSameSessionDoesNotDisplaceTheFirstTheAttachmentLedgerIsEmptyOnceARequestFinishesThat last row is honest rather than tidy. With the record never written, every release takes the never-added path and frees a handle the multi handle still holds, and I could not get that to show as anything:
curl_easy_cleanupdetaches the handle itself on the way out, so the state afterwards is identical either way. Measured on 8.14.1 under ASan and UBSan, adding a handle to a multi and performing untilrunning_handlesis 1, then freeing it while it is still attached:The multi drops it. Freeing in the other order,
curl_multi_cleanupfirst and then the attached handle and its header list, is also clean on that build. So the half of the accounting that's covered is the half that skips a removal it doesn't need, and the half that isn't is the documented call order, which I have not found a way to make bite on this libcurl.One case fixed that isn't about any of this
The valgrind job went red on
RepeatedCallerThreadCancelsAreClean, which came in with #4392 and has nothing to do with the accounting here. It sumsCancelledandResponseover twenty attempts against a closed port and asks for at least one each, and there's a third way such an attempt ends: the connection fails before the cancel arrives, and the cancel is then correctly a no-op. Instrumented, the silent attempts runCreated,Connecting,ConnectFailedand stop there.Pinned to one core, fifteen runs each:
3fb1d317Same on all four, so it isn't the change, it's a case that pinned one of two legitimate outcomes. It's now asking each attempt to end somewhere the handler is told about rather than to end the same way twenty times, and pinned to one core that goes from 15 of 15 failing to 0 of 15. The connection failure is counted apart from
terminal_count_, which two other cases pin to an exact value, and outside the existing chain, becauseACancelBeforeTheResponseReportsCancelledcancels fromConnectFailedand needs that branch to keep firing.It's a separate commit. It rides along because it's the job this PR makes red and it's a case I added, but it splits out cleanly if you'd rather have it on its own.
Checks
Measured on
7e6ed213, this branch merged withmainatac7b87a8.curl_http_testdetect_leaks=1bazel test //ext/test/http:curl_http_testThe suite is 46 rather than the 36 an earlier revision of this section reported,
because #4668 removed
WITH_OTLP_RETRY_PREVIEWand the cases that flag used togate now run in every build. That also retires the second configuration this
table used to carry, since it can no longer be set. clang-tidy and
include-what-you-use have CI jobs on every head and are not repeated here; their
limit moved from 73 to 204 while this sat, so a local figure from before that
would not compare.
Related
Rebased on current main. #4395 also touches
Cleanup()and is still open, and this revision doesn't go near it: the accounting is all inhttp_client_curl.ccand its header, and theCleanup()hunk is the one the first commit on this branch already had.About the line Codecov marks
Codecov reported 5 lines of this change without cover. One of them was the CA
certificate blob, uncovered because nothing in the suite supplied a certificate
as a string, and a case now does:
gcovrreports one hit afterwards. It setsuse_ssl, without which the whole SSL block is skipped and the case would passhaving exercised none of it.
The others are in the ledger's re-attach path and in a log on a branch that was
evaluated 34 and 62 times without being taken. Reaching them needs the removal
and the re-add to interleave in a particular order, which is a timing window
rather than a call, so I have left them uncovered rather than write a case that
would pin a schedule.
For significant contributions please make sure you have completed the following items:
CHANGELOG.mdupdated for non-trivial changes