Skip to content

[DESIGN] HTTP transport requirements and options - #4458

Open
thc1006 wants to merge 4 commits into
open-telemetry:mainfrom
thc1006:design/http-transport-4448
Open

[DESIGN] HTTP transport requirements and options#4458
thc1006 wants to merge 4 commits into
open-telemetry:mainfrom
thc1006:design/http-transport-4448

Conversation

@thc1006

@thc1006 thc1006 commented Aug 19, 2026

Copy link
Copy Markdown
Member

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 assume that HttpOperation has to go, that a Session carries one request, or where retry belongs. Those are listed as decisions.

What is in it

The three shapes named in the discussion, compared against the criteria named with them: throughput and connection reuse, cancellation and shutdown, who owns retry and concurrency, and compatibility with the installed interfaces and custom clients.

Before that, a table of what every in-tree consumer does today, read from main rather than from memory. That table is the load-bearing part of the argument, so here is where each row came from:

  • Zipkin takes a HttpClientSync, in zipkin_exporter.h and its factory.
  • Elasticsearch calls handler->waitForResponse() in Export, at es_log_record_exporter.cc:475.
  • OtlpHttpClient::Export takes a max_running_requests budget, and its own comment says that zero means a synchronous export; the default path passes zero and waits for the result.
  • WITH_ASYNC_EXPORT_PREVIEW is OFF at CMakeLists.txt:241.

So in the configuration almost everyone builds, 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 fact the rest of the document turns on.

What is deliberately not in it

No requirement numbering scheme, no benchmark matrix, no migration plan. You asked for something short, and the six decisions at the end are the part that has to be settled before any of that is worth writing.

The bug to invariant mapping is not repeated here either. It is the matrix already in the discussion thread, linked from the document, which also 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.md updated for non-trivial changes
  • Unit tests have been added
  • Changes in public API reviewed

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>
@thc1006
thc1006 force-pushed the design/http-transport-4448 branch from 5207f9e to d837491 Compare August 23, 2026 23:29
@codecov

codecov Bot commented Aug 24, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 83.12%. Comparing base (1c2b007) to head (0fea657).
⚠️ Report is 6 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #4458      +/-   ##
==========================================
+ Coverage   82.67%   83.12%   +0.45%     
==========================================
  Files         516      519       +3     
  Lines       20197    20256      +59     
==========================================
+ Hits        16696    16835     +139     
+ Misses       3501     3421      -80     

see 15 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…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>
@thc1006
thc1006 marked this pull request as ready for review August 27, 2026 16:35
@thc1006
thc1006 requested a review from a team as a code owner August 27, 2026 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant