[DESIGN] HTTP transport requirements and options - #4458
Open
thc1006 wants to merge 7 commits into
Open
Conversation
thc1006
force-pushed
the
design/http-transport-4448
branch
from
August 23, 2026 23:29
5207f9e to
d837491
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4458 +/- ##
=======================================
Coverage 83.47% 83.47%
=======================================
Files 522 522
Lines 20440 20440
=======================================
Hits 17061 17061
Misses 3379 3379 🚀 New features to boost your workflow:
|
thc1006
marked this pull request as ready for review
August 27, 2026 16:35
thc1006
force-pushed
the
design/http-transport-4448
branch
from
September 8, 2026 05:27
0fea657 to
16db2fc
Compare
Written for the question in open-telemetry#4448: does the HTTP transport need to manage asynchronous concurrency itself, or is a request and result model enough with concurrency owned somewhere else. Sets out what every in-tree consumer actually does, where the current model is inconsistent with itself, and compares the three shapes named in the discussion: the current Session and EventHandler model, an implementation neutral request and result, and a single owner CURLM event loop for the case where transport level concurrency is wanted. It deliberately does not decide whether HttpOperation should exist, whether a Session carries one request, or which layer owns retry. Those are listed as decisions instead, together with the CMake and Bazel surfaces that disagree about the concrete curl headers today. The evidence is the invariant matrix already posted in open-telemetry#4448, linked rather than repeated, so this stays short enough to read in one sitting. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…ocument The first draft turned on one fact, that every default consumer blocks, and read as an argument for dropping the asynchronous machinery. @owent supplied the other half on open-telemetry#4448: a high volume logging workload with roughly 100 ms of network latency and batches past 4 MB, where a collector that was not saturated still dropped data until about four requests were allowed in flight. That does not contradict the first fact but it does change what follows from it. Concurrency is a capability the transport has to have; what the blocking callers want is not to pay for it. So the document now asks for a contract where it is available, bounded and configurable rather than for one that picks a side, and option B is an asynchronous one attempt operation with blocking as an adapter over it, not a blocking call. Also corrected: the specification's wording differs by transport, and this document is about HTTP. Concurrent unary calls are a SHOULD for gRPC. For HTTP the text is that a client MAY use several parallel connections, with the maximum SHOULD be configurable. Adds the benchmark matrix the concurrency question needs, because the only OTLP HTTP benchmark in the tree runs against localhost with a 1 ms timeout, and records the open question that the option defaults to 64 requests in flight while the one reported workload needed about four. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
…ution holds @owent's second reply on open-telemetry#4448 says the two options were written around curl and that a replacement is not bound by them. Reading them says what a replacement would have to keep, and it is less than the names suggest. max_concurrent_requests is a watermark applied after the request has been built and handed to the client, not an admission limit. max_requests_per_connection counts nothing per connection: every Nth session by global id sets CURLOPT_FRESH_CONNECT and CURLOPT_FORBID_REUSE, which is a rotation rather than a quota. Neither shapes what is on the wire, because the client sets none of CURLMOPT_MAX_HOST_CONNECTIONS, CURLMOPT_MAX_TOTAL_CONNECTIONS, CURLMOPT_MAX_CONCURRENT_STREAMS, CURLMOPT_PIPELINING or CURLOPT_HTTP_VERSION. The same reply says requests added to one CURLM without disabling reuse can look concurrent while running serially. That holds on one connection that cannot multiplex, which is the HTTP/1.1 case, and not on several connections or on HTTP/2 and HTTP/3 streams. The document now says which, because it decides how many numbers the replacement needs rather than one. Also adds a byte budget and a separate export operation and attempt identity to the decision list, and makes the benchmark name its rate units and read back the negotiated protocol and connection count rather than trusting the option values. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The comparison table said option C keeps retry and concurrency inside the backend while the recommendation put a scheduler above the transport owning exactly those. One row cannot answer for both, so it is now two: admission and retry above, connection and stream scheduling below. max_concurrent_requests currently stands for a record rate, an attempt count and a connection count depending on who is reading it, so the document now names those apart before comparing anything. The reported workload is a record rate; "about four" is an attempt count. The byte bound, the separation of export operations from attempts, and exactly-once settlement move out of the open questions and into requirements, since no answer to the remaining questions makes them optional. The decision list splits into what open-telemetry#4448 has settled and what is still open, so a reviewer can object to one line rather than re-read the thread. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
18231989 settles two things the open list still asked. The existing options are legacy curl compatibility controls rather than requirements of a replacement, and a bounded governor needs both a count and a retained-byte dimension rather than either alone. It also states a preference for one shared CURLM while leaving the sharing scope to the benchmark. 18233325 settles a third. Session::SendRequest was never meant to be called more than once, which is the contract the use after free measured on main violates, so the document states it instead of asking it. What sharing a multi handle costs now has its own section: a reference counted runtime under separate logical clients rather than a global HttpClient, a hierarchical budget because one per-client limit stops bounding the process once three exporters share it, a fairness requirement, and a failure domain, since libcurl leaves every transfer on a multi handle undefined when socket_action fails. The benchmark grows the cases that measure isolation rather than speed. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
The vocabulary table already named an export operation and an attempt as different things without saying that they move through different states. They do, and today they share one set of fields, which is where several of the reports come from: a retried operation gets a second attempt with its own deadline, handle and result, and if those live on the operation the second overwrites the first. Each boundary in the attempt sequence is a report already filed, from a rejected curl_multi_add_handle that never runs to CURLOPT_PRIVATE naming a Session rather than the attempt whose completion is arriving. The second section names three accepted issues the rewrite does not fix, so the scope is not read wider than it is. Processor batching decides the request geometry the transport is handed, and open-telemetry#4498 measured 2,332 export calls for 50,000 records with one batch at or above 2,048, so a benchmark run before that is settled reports the processor. open-telemetry#4532 shows an untimed join above the exporter, so a deadline honoured by every attempt still does not bound Shutdown. open-telemetry#4534 is a throw crossing a noexcept callback, which the completion contract has to answer for, including where there are no exceptions to catch. One word elsewhere: an export operation is accepted, not admitted, since the table reserves admitted for attempts the governor has let start. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
thc1006
force-pushed
the
design/http-transport-4448
branch
from
September 8, 2026 07:33
16db2fc to
76fb5e3
Compare
The option is `OTELCPP_WITH_ASYNC_EXPORT_PREVIEW`. The old spelling still works but is deprecated, and the document was citing it. "The configuration almost everyone builds" was a claim about how many people build what, which I cannot observe. What is checkable is that the option is off by default, so the sentence now says the default configuration and nothing more. "Each boundary in the second sequence is a report" was not true of the sequence I had just written. It has seven boundaries and four bullets follow it, one of which is not a boundary at all. Four of the open reports are about that sequence, so it says that instead. Signed-off-by: thc1006 <84045975+thc1006@users.noreply.github.com>
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.
This is the requirements and options document @lalitb asked for in #4448, as a pull request so it can be reviewed before anyone chooses an implementation direction.
It answers the question that was actually asked, which is whether the HTTP transport needs to manage asynchronous concurrency itself or whether a request and result model is enough with concurrency owned somewhere else. It does not propose class names, and it does not decide where retry belongs, which is one of the open decisions at the end. Whether
HttpOperationsurvives is written down as @owent's preference with the evidence for it, not as a conclusion.What is in it
The three shapes named in the discussion, compared against the criteria named with them: throughput and connection reuse, cancellation, shutdown, who owns admission and retry, connection and stream scheduling, and what happens to the installed interfaces.
Before that, a table of what every in-tree consumer does today, read from
mainrather than from memory. That table is the load-bearing part of the argument, so here is where each row came from:HttpClientSync, inzipkin_exporter.hand its factory.handler->waitForResponse()inExport, ines_log_record_exporter.cc.OtlpHttpClient::Exporttakes amax_running_requestsbudget, and its own comment says that only a sync export, meaning zero, needs to capture the result of the callback. The default path passes zero and waits.OTELCPP_WITH_ASYNC_EXPORT_PREVIEWisOFFby default in the top levelCMakeLists.txt.So in the default configuration every exporter blocks until its request is done, and two of the three do it by handing the request to an asynchronous client and then waiting. That is the first of the two facts the document turns on. The second is @owent's production evidence in #4448, which is why the document does not conclude that the asynchronous machinery should go.
What is already settled
Three questions are recorded as settled rather than left open, on the strength of @owent's replies on #4448:
max_concurrent_requestsandmax_requests_per_connectionare legacy curl compatibility controls. A replacement is not bound by their semantics, only by the goals behind them.Session::SendRequestwas never meant to be called more than once. That is the contract violated by the use after free I measured onmainand reported in Proposal: Redesign the curl HttpClient API to address thread-safety and lifetime issues #4448.One shared
CURLMis now written as the preferred default for the curl backend rather than one option among three, with its sharing scope left to the benchmark. What that sharing costs has its own section: a reference counted runtime beneath separate logical clients rather than a globalHttpClient, a hierarchical budget, a fairness requirement, and a shared failure domain.Two sections come from reading the reports together rather than from the discussion. An export operation and an HTTP attempt are given separate state sequences, because a retried operation makes a second attempt with its own deadline, handle and result, and four of the open reports are about points where that second sequence is not respected today. The other section names what a transport rewrite does not fix: processor batching in #4449 and #4498, which decides the request geometry the transport is handed and so gates the benchmark; the untimed join in #4532, so a deadline honoured by every attempt still does not bound
Shutdown; and #4534, a throw crossing anoexceptcallback, which the completion contract has to answer for even where there are no exceptions to catch.What is deliberately not in it
No requirement numbering scheme and no migration plan. You asked for something short, and the open decisions at the end are what has to be settled before either is worth writing.
The bug to invariant mapping is not repeated here. It is the matrix already in the discussion thread, which the document points back to, and which marks which reports are measured and which are only reasoned.
On the recommendation section
There is one, it is labelled as an opinion, and it is meant to be argued with rather than accepted. Short version: define the one attempt semantics as the contract, keep the current interface working through an adapter, and treat the single owner event loop as the curl backend's implementation for the case where concurrency is actually wanted. I would also want the concurrency question settled by a measurement rather than by assumption.
If the shape of the document is useful but the recommendation is not, the recommendation is one section and can go.
For significant contributions please make sure you have completed the following items:
CHANGELOG.mdupdated for non-trivial changes