Conversation
…a non-responding client The synchronous export path waited on its response condition variable with no deadline of its own, entirely trusting the injected HttpClient to eventually deliver a terminal event via OnResponse or OnEvent. Nothing in the HttpClient interface actually guarantees that: a client that accepts a request and never calls back (a dead thread, a reused socket, a swallowed error) left Export() blocked for the life of the process, with no way for a caller's Shutdown() to release it either. waitForResponse() now takes an absolute deadline, derived from the exporter's own configured response timeout and captured before the request is sent, so the wait is bounded independent of whether the client honors its side of the contract. A deadline that passes without a terminal event reads as failure, the same outcome a terminal error event would already produce, so no successful path changes. Added a SilentHttpClient/SilentSession test double whose SendRequest() never calls back into its handler at all, and verified the regression test actually catches the bug: reverting the fix locally makes the test hang and get killed by its own timeout wrapper (exit 124), rather than passing vacuously. Fixes open-telemetry#4362
83187ea to
37d54e4
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4530 +/- ##
==========================================
+ Coverage 86.51% 86.52% +0.01%
==========================================
Files 525 525
Lines 20464 20475 +11
==========================================
+ Hits 17702 17713 +11
Misses 2762 2762
🚀 New features to boost your workflow:
|
…elated to this change)
mateenali66
left a comment
There was a problem hiding this comment.
Ran this against the curl client with response_timeout = 1. A server that never replies fails the export at about 1s on both main and this PR, and replies at 990 to 999ms split the same way. So the change only matters for a custom client.
For that case, Export() still calls session->FinishSession() after the deadline (:495). SilentSession::FinishSession returns at once, so the test can't see it. A client built like curl waits there on the transfer (http_operation_curl.cc:523-538), so a dead worker thread still hangs Export(). curl's CancelSession() does not block (http_client_curl.cc:255-262). Should the expired path cancel instead?
The conflict is only CHANGELOG.md.
FinishSession() waits for the in-flight transfer to complete, which defeats the purpose of the response deadline for HTTP clients (e.g. curl) whose worker thread blocks on the transfer itself. Cancel the session instead when the deadline expires. Addresses review feedback from open-telemetry#4530 (review)
|
Good catch, and sorry for the delay replying. This was actually fixed same-day in Added |
mateenali66
left a comment
There was a problem hiding this comment.
No gap in the timeout path, that one holds. Built the head and swapped the silent session for one that delivers a prompt ConnectFailed. It cancels too: 0 ms elapsed, cancel called, finish not, with response_timeout at 30.
The branch is on write_successful, not on deadline expiry, so ConnectFailed, SendFailed and CreateFailed take it as well. Those record Failure promptly with the transfer already over, and for curl the two calls differ. FinishSession waits on the result future, CancelSession aborts and does not. So three paths stop reaping the transfer, which is more than the comment above the branch claims.
Pending is already the signal. A deadline expiry leaves completion_ there, every other failure sets Failure. Worth having waitForResponse report which of the two it left on?
The cancel-vs-finish branch was keyed on write_successful, so any terminal failure (ConnectFailed, SendFailed, CreateFailed) also took the CancelSession() path even though the transfer was already over by then. For curl, CancelSession() and FinishSession() are not interchangeable in that case. waitForResponse() now reports via an out-param whether the deadline passed with completion_ still Pending (a genuine still-outstanding transfer, which needs cancelling) as opposed to a terminal event having already arrived (which needs the normal FinishSession()). Added ExportFinishesSessionOnTerminalFailureInsteadOfCancelling with a FailFastSession/FailFastHttpClient pair that delivers ConnectFailed promptly, asserting FinishSession() is called and CancelSession() is not, and that the export returns well under the response_timeout_. Addresses review feedback from mateenali66.
|
Confirmed, the branch was keyed on write_successful rather than on the deadline itself. Fixed in `295ec739`: waitForResponse() now reports via an out-param whether completion_ was still Pending when the deadline passed, and only that case cancels; a terminal failure (ConnectFailed, SendFailed, CreateFailed) finishes the session normally since the transfer is already over. Added ExportFinishesSessionOnTerminalFailureInsteadOfCancelling with a FailFastSession that delivers ConnectFailed immediately, asserting FinishSession() runs and CancelSession() does not, and that the export returns well under the 30s response_timeout_. On the waitForResponse() reporting which state it left on: went with an out-param (timed_out) rather than exposing CompletionState directly, so the caller only sees the one bit it needs. Let me know if you'd rather have the full state exposed for something else. |
Fixes #4362.
The synchronous export path waited on its response condition variable with no deadline of its own, trusting the injected
HttpClientto always eventually deliver a terminal event viaOnResponseorOnEvent. Nothing in theHttpClientinterface actually guarantees that: a client is a supported public surface, not a test seam, and one that accepts a request and never calls back (a dead thread, a reused socket, a swallowed error) leftExport()blocked for the life of the process, with no way for a caller'sShutdown()to release it either.Changes
waitForResponse()now takes an absolutestd::chrono::steady_clock::time_pointdeadline instead of waiting unconditionally, viacv_.wait_until()in place ofcv_.wait().response_timeout_and captured beforeSendRequest()is called, so it reflects this exporter's own timeout budget rather than whatever the client does with it.Pending, which reads as failure, the same outcome a terminal error event already produces today. No successful path changes.Testing
Added
SilentHttpClient/SilentSessiontest doubles whoseSendRequest()never calls back into the handler at all (noOnResponse, noOnEvent), the exact scenario the issue describes.ExportReturnsOnTimeoutWhenClientNeverRespondsconstructs the exporter with a 1 secondresponse_timeout_and this client, and assertsExport()returnskFailurerather than hanging.Verified the test actually catches the regression: reverted the fix locally (kept the test) and reran under a
timeoutwrapper, the test hung and was killed at exit code 124 instead of passing vacuously. Restored the fix and confirmed all four tests in the file pass, total runtime 1 second (the deadline in the new test), not 30 (the defaultresponse_timeout_).