Skip to content

test: drip server closes the mid-response streaming coverage gaps - #186

Merged
puzza007 merged 2 commits into
masterfrom
drip-test-server
Jul 18, 2026
Merged

puzza007 merged 2 commits into
masterfrom
drip-test-server

Conversation

@puzza007

Copy link
Copy Markdown
Owner

The local httpbin stack buffers whole responses behind its proxy, so two streaming behaviors have been test-blind since the streaming work landed — reachable only in the TLA+ model, never against a real socket: a terminal error arriving after chunks were already delivered, and a cancel landing between headers and done. Both were disclosed as accepted gaps in #182.

katipo_drip_server closes them: a ~90-line single-connection HTTP/1.1 responder that can advertise a larger Content-Length than it sends (closing early yields a deterministic partial_file mid-body), deliver timed body pieces so a response is genuinely in progress when the interesting event happens, stall with the socket open, and report peer-close to the owning test. It's linked to the test process, so failures tear it down.

Three new cases in the async group. streaming_error_mid_body: headers, at least one chunk, then the terminal partial_file — the error-after-chunks sequencing previously only model-verified. streaming_timeout_mid_body: headers and a chunk, then operation_timedout while the server stalls. And streaming_cancel_mid_stream, the strongest of the set: after a cancel between headers and done, in-flight chunks may drain but no terminal message ever arrives — and the stalled server's recv observes the connection close, the end-to-end proof that cancel_conn in the C port actually aborts the transfer rather than merely having its late output dropped. That C path had never been exercised against a live socket.

The suite also gains expect_stream_headers/expect_first_chunk helpers shared with the previously inline receives in streaming_flow_control. 202 tests, dialyzer, xref, and lint pass.

Top of the stack: #180 → #182 → #183 → #184 → #185 → this. Merge bottom-up; I'll rebase each layer as the one below lands.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Vf2gxZLpxT4HvBfZKCgP3g

@codecov

codecov Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 78.52%. Comparing base (bec37ad) to head (e14096b).

Additional details and impacted files
@@                      Coverage Diff                      @@
##           bounded-admission-timeout     #186      +/-   ##
=============================================================
+ Coverage                      78.23%   78.52%   +0.29%     
=============================================================
  Files                              9        9              
  Lines                           1369     1369              
=============================================================
+ Hits                            1071     1075       +4     
+ Misses                           298      294       -4     
Flag Coverage Δ
c 78.52% <ø> (+0.29%) ⬆️
erlang 78.52% <ø> (+0.29%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

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

@puzza007
puzza007 force-pushed the bounded-admission-timeout branch from a83b638 to 47b9148 Compare July 18, 2026 08:49
@puzza007
puzza007 force-pushed the bounded-admission-timeout branch from 47b9148 to ba12838 Compare July 18, 2026 08:59
@puzza007
puzza007 force-pushed the bounded-admission-timeout branch from ba12838 to 170d439 Compare July 18, 2026 09:05
@puzza007
puzza007 force-pushed the bounded-admission-timeout branch from 170d439 to 9261944 Compare July 18, 2026 09:09
puzza007 added 2 commits July 18, 2026 21:37
…llers

The admission handshake introduced with the exactly-once dispatch work
used an infinity gen_server:call, so a wedged worker -- typically its C
port stopped draining the pipe and port_command blocked the worker
mid-callback -- silently absorbed async_req callers with no bound at
all. Admission is a port write plus a map insert (microseconds when
healthy), so bound the call at 5s: a worker that cannot answer in time
yields an immediate {error, #{code => admission_timeout}} and the caller
can retry, re-rolling onto a healthy random worker, instead of blocking
indefinitely. A request the worker admits after the caller gave up is
cleaned up by its own request timer.

The bound applies only to the async admission ack and is derived from
the message kind in one place (admission_bound/1): a synchronous
request's single call carries the whole response -- the worker replies
when the transfer or its request timer completes -- so it stays
unbounded at the call layer and is governed by the request timeout as
before. attempt/2 guards its timeout mapping on the bound, since on an
unbounded call a {timeout, _} exit can only be a worker that died with
that exit reason. The fail-fast contract is uniform: a timeout during
overload-spillover also returns immediately, so an admission waits at
most one bound, never one per candidate.

Deterministic coverage in katipo_async_failure_SUITE: a suspended
worker (the messages-queue-but-nothing-runs shape of the production
wedge) must produce admission_timeout within the bound. 199 tests,
dialyzer, xref, and lint pass.
The local httpbin stack buffers whole responses behind its proxy, so
two streaming behaviors were only reachable in the TLA+ model, never
against a real socket: a terminal error arriving after chunks, and a
cancel landing between headers and done. katipo_drip_server is a
minimal single-connection HTTP/1.1 responder that makes both
deterministic: it can advertise more Content-Length than it sends
(close -> partial_file mid-body), deliver timed body pieces so a
response is genuinely in progress, stall with the socket open, and
report peer-close to the owning test.

Three new cases: streaming_error_mid_body (headers, at least one chunk,
then partial_file), streaming_timeout_mid_body (headers and a chunk,
then operation_timedout while stalled), and streaming_cancel_mid_stream
-- which also asserts the stalled server's recv observes the connection
close after the cancel, the end-to-end proof that the C port aborts the
transfer rather than merely having its late output dropped, covering
cancel_conn against a live socket for the first time.

The suite gains expect_stream_headers/expect_first_chunk helpers shared
by the new tests and streaming_flow_control's previously inline
receives, and the cancelled-stream silence check drains in-flight
chunks then delegates to the existing assert_stream_stopped contract
helper. 202 tests, dialyzer, xref, and lint pass.
@puzza007
puzza007 force-pushed the bounded-admission-timeout branch from 9261944 to bec37ad Compare July 18, 2026 09:37
Base automatically changed from bounded-admission-timeout to master July 18, 2026 09:41
@puzza007
puzza007 merged commit 950da2f into master Jul 18, 2026
12 checks passed
@puzza007
puzza007 deleted the drip-test-server branch July 18, 2026 09:42
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