Replies: 5 comments
|
I think the issue set now justifies redesigning the curl implementation, but I would separate agreeing on the contract from rewriting the code. The failures no longer look independent. They cluster around a few common boundaries:
My preference would be to preserve the abstract HttpClient / Session / Request surface initially and replace the curl implementation behind it in stages. Before implementation, I think we need to decide at least these contracts:
For the implementation, I would prefer one transfer object with strong ownership of its immutable request data, callback and curl resources. User threads would enqueue commands, while the I/O thread would be the only thread that mutates libcurl handles and transfer state. State would be committed before invoking user code, with no internal client lock held across a callback. Merging HttpOperation into Session works if Session is formally single-use. If Session remains reusable, I think the operation still needs an identity of its own, and CURLOPT_PRIVATE should identify the exact operation rather than a Session whose current operation may already have changed. I would keep #4395, #4405, #4406 and #4431 as drafts while this is decided. The reproductions and sanitizer cases in them are still valuable and can become the acceptance suite for a replacement. Narrow fixes that are independently correct under either design can still land if we need them before the replacement. One correction to #4433: #4327 stopped installing the concrete curl headers through CMake, although Bazel //ext still exposes them. The measured dangling-reference bug remains, but the current external surface is narrower than I wrote there. I also agree that copying is not the right end state, and that moving or consuming immutable request state avoids both the copy and the borrowed lifetime. Correcting one thing I wrote on #4433 about my own patch: it moves at the in-tree call site rather than copying, and the by value parameters are what let it do that without changing a call site. The ownership question it raises still belongs here rather than in a patch. I can put together an invariant/issue/test matrix from the existing reports and use that as the starting point for a design sketch. |
|
Here is the matrix I offered. It is built from the reports rather than from opinion, so that the design argument can start from what actually broke. How to read it. Each row is an invariant the replacement has to hold, phrased so that a case can fail when it does not. Evidence is the report that showed it broken, and says whether that report carries a sanitizer result or a deterministic reproduction (measured) or is derived from control flow alone (reasoned). Pinned by is the case that fails today if the invariant breaks, and which draft it currently lives on. Rows with no case are collected at the end, and they are the part I would look at first. I have kept the invariants at the level of "what must be true", not "how it should be built". Several of them are satisfied by more than one design, including yours. A. Publication and ownership
B. Terminal outcome
C. Callbacks and re-entrancy
C5 is the one I would not have predicted. D. libcurl handle ownership
On D4, I measured the current order on libcurl 8.14.1 under ASan and UBSan and saw nothing reported, and the manual says E. Worker lifecycle
F. Allocation and failure paths
G. Request lifetime
H. Retry
What nothing holds todayThese are the rows above with no case, plus one measured coverage gap.
The seven questions, and what forces each one
What I would do with thisTwo things follow that I think are worth deciding before any code. The first is that C1 and C5 together decide the shape of the whole thing. If callbacks for one request are serialised and log lines count as callbacks, then no internal state may be in flux while user code runs, and the state machine has to commit before it calls out. If callbacks are allowed to overlap, then every handler in the ecosystem has to be re-entrant, which the current interface never asked for. The second is that the gap list is the acceptance suite for a replacement, not a backlog. Nine invariants have no case. A replacement that keeps the existing cases green tells us nothing about those nine, and they include the worker lifecycle and the one path that sends after reporting failure. If this shape is useful I will turn the gap list into cases against the current implementation first, so they fail on |
|
@owent @thc1006 - Thanks for initiating the discussion, and also collecting these reports and the invariant matrix. I agree the current implementation has issues, and several of these issues also affect the default build, without the async preview enabled. However, before redesigning the current asynchronous implementation, I suggest that we step back and confirm what we actually need from the HTTP transport. Concurrent HTTP export is still preview functionality and is disabled by default. In the normal configuration, OTLP/HTTP and Elasticsearch use the asynchronous curl client but then wait for completion, while Zipkin already uses the synchronous client. We therefore carry the background-thread, callback, and The OTLP specification requires bounded export time and retry behavior, but parallel HTTP requests are optional. This raises the main design question: Does the HTTP transport itself need to manage asynchronous concurrency, or could we use a simpler request/result model with optional concurrency managed at another layer? The abstraction should remain independent of curl. Exporters should depend only on a clear transport contract, allowing curl to be one implementation alongside custom clients or platform-native implementations such as WinHTTP or I suggest preparing a short requirements-and-options document comparing the current model, a simpler implementation-neutral request/result transport, and - if transport-level concurrency is required - a single-owner We can review the document as a pull request before choosing the implementation direction. Until then, I suggest keeping the overlapping implementation PRs in draft while focused reproductions and independent correctness fixes continue. The gzip failure fallthrough and #4425 can proceed independently of this design decision. Edit - additional context from earlier work:
|
|
Thanks, and yes to stepping back. Taking your direct question first, because I think the evidence answers it. The default configuration pays for machinery it does not use. OTLP HTTP and Elasticsearch both hand a request to the asynchronous client and then wait for it, Zipkin uses the synchronous one, and the async preview is off by default. Everything in the invariant matrix under publication, callback re-entrancy and worker lifecycle exists to make concurrency safe, and none of it is needed by a caller that is going to block anyway. So my answer is that transport level concurrency looks like an optional capability rather than the shape the interface should be built around. What I cannot answer from the reports is whether removing it costs throughput that someone depends on, which is the part worth measuring rather than asserting. Three tracks, then. I will put up the requirements and options document as a pull request. You asked for a short one and I will keep it short. The three options you named are the ones to compare, and I will not assume that The two independent fixes go up as small separate pull requests, #4425 first and then the gzip fall through. Neither touches the curl client's structure and neither pre-commits the design. #4395, #4405, #4406 and #4431 stay in draft with their scope frozen. I will not rebase them for the sake of looking current. Their deterministic reproductions and sanitizer cases are the part worth keeping whichever direction this goes, and the matrix marks which invariant each one holds. Thank you for the history in your edit. #6, #370, #1175 and #1243 are the context I was missing, and knowing that the asynchronous work came in for throughput, connection reuse and avoiding a thread per request tells me what the document has to weigh rather than dismiss. |
|
I believe async concurrency is necessary. A few years ago, back when the HTTP exporter had no async support, we frequently ran into situations where the exporter couldn't keep up with the rate at which data was being produced (especially in high-volume logging scenarios), resulting in massive drops. After investigation, the bottleneck turned out not to be the server, but network latency (around 100ms). By that point, the BatchProcessor had already merged a large amount of data — each submission exceeded 4MB. The actual problem was that consumption simply couldn't keep pace with production. The drops only disappeared once we raised the concurrency to around 4. |
Uh oh!
There was an error while loading. Please reload this page.
It seems that ext::http::client::curl::HttpClient and the related code still suffer from numerous multithreading synchronization issues and lifetime-safety problems, even after multiple rounds of patches. Would it be possible to consider redesigning the entire API flow — re-evaluating which data is accessed only by the background thread, which is accessed only by the user thread, which is shared, and which data requires shared ownership/lifetime? Additionally, could reentrancy protection be applied to all user-facing interfaces?
Related issues: #4438, #4408, #4403, #4402, #4397, #4396, #4393, #4391, #4360
Do you think it would be worthwhile to completely redesign and rewrite this component?
@open-telemetry/cpp-approvers @open-telemetry/cpp-maintainers @thc1006
All reactions