Skip to content

Completion tokens: complete through the caller's immediate executor, and tests - #41

Merged
pgit merged 6 commits into
masterfrom
completion-tokens
Nov 7, 2026
Merged

pgit merged 6 commits into
masterfrom
completion-tokens

Conversation

@pgit

@pgit pgit commented Nov 7, 2026

Copy link
Copy Markdown
Owner

Completion-token API (ASIO): completions honour the caller's immediate executor, and the token layer gets tests of its own.

  • fix: complete immediately through the caller's immediate executor. complete_immediately() wrapped the handler in a lambda, so async_immediate() never saw an immediate executor the caller had bound, and every completion was posted. It now runs on the handler's own immediate executor, and posts only when there is none. Error paths now complete the same way as success paths: the client's submit and get_response used to call their handler inline when they failed and now post it, as they do when they succeed. A server's Response::async_submit() over HTTP/2 and HTTP/3 stays inline on all paths: queueing that completion cost 16–41% of throughput (measurements in docs/benchmark.md). complete_later() is now complete(). Recursion now requires the second read to nest deeper.
  • test: the completion-token API. test_asio.cpp runs async_submit(), async_shutdown() and the rest with as_tuple, cancel_after, the default token and plain callbacks, over all three protocols. test_quic.cpp adds cancel_after on async_accept_stream(). An empty handle has no executor, which is now documented on Reader::get_executor().
  • test: Session::Impl's default shutdown() reports operation_not_supported.
  • chore: a build-coverage preset for the published report. The Coverage job now uses it, and docs/ is left out of the report.
  • test: spell Boost's asio in full. This repairs the two test files above after the rebase onto master: a bare asio:: is now anyhttp's runtime namespace. It can be squashed into the completion-token commit.
  • test: fix a flaky test. ClientTlsEnd…WHEN_write_is_stuck_THEN_shutdown_interrupts_it_and_says_so/HTTP2 failed about 1 run in 4 under gtest-parallel -w 100. It assumed the write was stuck 50 ms in, but a peer that stops reading keeps a small window open for about 200 ms on loopback. The peer now waits until its advertised receive window (tcpi_rcv_wnd, read in the new test/tcp_info.cpp) is 0 on two samples in a row. Result: 600/600 runs pass on ASIO with -w 200, and 200/200 on COROSIO.

Tested: the full suite under gtest-parallel in build/ (ASIO) and build-corosio/, and build-gcc compiles test_all.

🤖 Generated with Claude Code

pgit and others added 6 commits November 7, 2026 20:19
The Coverage job configured its tree by hand; it now uses the build-coverage preset, which
reproduces the report locally. docs/snippets.cpp is linked into test_all to be run, not to be
measured, so docs/ joins test/ among the paths the report leaves out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
No backend that hands out a Session leaves the default in place, so a session with nothing behind
it stands in for one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
complete_immediately() handed asio::async_immediate() a lambda around the handler, and
async_immediate() asks what it is given for its immediate executor: the lambda has none, so an
immediate executor the caller bound was never fetched and every completion was posted. It now
executes on the handler's own, falling back to a post as before, so the caller decides whether it
is completed inline. So does the default async_shutdown_impl() of any_async_stream. The second
half of the Recursion test had passed regardless, through the extra frames of the binder's path;
it now reads twice and requires the second read to nest deeper (HTTP/2 and HTTP/3 only, HTTP/1.1
reads in a task of its own and never completes immediately).

Each operation completes the same way on all of its paths. The client's submit and get_response
called their handler inline when they failed, and post it now, as they do when they succeed. A
server's Response::async_submit() over HTTP/2 and HTTP/3 completes inline on all paths: the
responses of requests that arrived together go out in one send only if their handlers have all
written before the write pass runs, and queueing the completion, ahead of the pass or behind it,
cost 16-41% of throughput. docs/benchmark.md has the measurements.

complete_later() is complete() now, being the default way to complete.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
test_asio.cpp runs async_submit(), async_shutdown() and the rest with as_tuple, cancel_after,
the default token and plain callbacks, over all three protocols, and cancel_after on a shutdown
that waits for a peer that never answers. test_quic.cpp adds cancel_after on async_accept_stream().
An empty handle has no executor, so a callback completes inside the call and cancel_after cannot
start its timer: documented on Reader::get_executor().

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Since the namespaces are named after their directories, a bare asio:: in a test is anyhttp's inline
runtime namespace, and test_asio.cpp and test_quic.cpp no longer compiled after the rebase onto
master.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WHEN_write_is_stuck_THEN_shutdown_interrupts_it_and_says_so/HTTP2 failed about one run in four
under gtest-parallel -w 100. It called shutdown() 50ms in, taking the write to be stuck by then,
but a peer that stops reading keeps a small window open for some 200ms on loopback, and a client
that got little CPU had not filled the pipe yet: its DATA frame completed, the GOAWAY fit in after
it, and the session ended cleanly -- correctly reported as success.

The peer now waits for its advertised receive window to be 0 on two samples 10ms apart before it
lets the client shut down: that window stays closed, and the client has filled its send buffer
behind it. A receive queue that has stopped growing is no proof of that, it stands still for tens of
milliseconds at a time. tcpi_rcv_wnd is read in tcp_info.cpp, as <linux/tcp.h> clashes with the
<netinet/tcp.h> Asio includes, and glibc's struct tcp_info ends before that field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@pgit
pgit merged commit 2fad5b7 into master Nov 7, 2026
12 checks passed
@pgit
pgit deleted the completion-tokens branch November 13, 2026 20:38
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