Skip to content

🧪 Keep the HTTP journal test's teardown off the fetch pool's clock - #411

Merged
taras merged 1 commit into
mainfrom
guarded-journal-http-teardown
Aug 9, 2026
Merged

🧪 Keep the HTTP journal test's teardown off the fetch pool's clock#411
taras merged 1 commit into
mainfrom
guarded-journal-http-teardown

Conversation

@taras

@taras taras commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Why

packages/core/tests/guarded-journal.test.ts's HTTP-backend case intermittently stalled Deno test teardown — an exactly-3m0s test solo, and past the 20-minute deadline under deno task verify's concurrent battery (#407). The stall is in DurableStreamTestServer.stop(): under Deno's node:http polyfill, server.close() waits for a connection only the same process's fetch pool can release.

What changes

Before:

The test server bound the package's fixed default port 4437, and the HTTP client used the runtime's pooled keep-alive fetch. During the test's request burst, Deno's fetch client sometimes opens a spare connection that never carries a request. At teardown, the polyfill's closeIdleConnections() refuses to reap it (it only destroys sockets that finished a response), close() tears down the 30s reaper interval that would have expired it, and stop() then waits on the client pool's eviction clock: ~180s when hyper-util's 90s idle-eviction interval fires (the "exactly 3m0s" runs), unbounded when it does not (the ≥20m battery timeouts; one wedged deno test process on the reproduction machine was 3.5 days old). The fixed port also let concurrent suites share one listen port on macOS — three runtimes' servers bound 127.0.0.1:4437 simultaneously without EADDRINUSE across 120 overlapping runs.

After:

The server binds an ephemeral port (port: 0), and every request the test's client sends carries Connection: close, so no connection outlives its response and stop() has nothing to wait on. Teardown still fully awaits server.stop(); the 20-minute deadline and the concurrent battery are untouched.

How to verify it

  • Reproduction (unfixed, same machine, same commit): deno test --allow-all --frozen packages/core/tests/guarded-journal.test.ts stalled at teardown and completed at exactly 3m0s — 3 of 3 iterations under concurrent 3-runtime load, 2 of 5 solo shortly after, 2 of 12 instrumented runs. A stalled process held both ends of one ESTABLISHED self-connection to 4437 with no listener, and an in-process dump showed the polyfill tracking one socket with _httpMessageDetached unset and an active entry with headersCompleted: false, req: null — a connection that never carried a request. Calling closeIdleConnections() did not release close(); closeAllConnections() did.
  • Fixed: 40/40 Deno runs at 0–1s with Node and Bun looping the same test concurrently (40/40 each, no failures), while a same-machine unfixed baseline kept stalling (6 of 10 runs at 180–182s). Full deno task verify on this branch: every command green, Deno leg 520.5s against the 20m deadline (test:node 254.9s, test:bun 370.1s).
  • All three runtimes transmit the Connection: close request header (verified empirically; Deno and Node close the socket immediately, and Bun's close() does not wait on connections).

Scope

Included

  • Test-only change to the HTTP-backend case's server construction and client fetch.

Intentionally unchanged

  • useHttpDurableStream keeps pooled keep-alive fetch in production: serialized appends over one connection are the intended durable-execution behavior; connection reuse is incidental to this test, whose subject is gate rejection.
  • DurableStreamTestServer.stop() (vendored @durable-streams/server) is not patched; its close()-only shutdown and Deno's polyfill gap are candidates for upstream reports, noted on 🧪 Deno HTTP journal teardown intermittently stalls concurrent verification #407.

The HTTP-backend case binds the test server to an ephemeral port and sends
Connection: close on every request. Deno's fetch client can open a spare
keep-alive connection that never carries a request, and the node:http
polyfill's close() waits for it — closeIdleConnections() only reaps sockets
that finished a response, and close() destroys the reaper interval that
would have expired it — so stop() waited on the client pool's 90s eviction
interval (observed as an exactly-3m0s test) or, when eviction never came,
past the verification deadline. A fixed default port also let concurrent
suites share one listen port on macOS instead of refusing the second bind.

Closes #407
@github-actions

github-actions Bot commented Aug 9, 2026

Copy link
Copy Markdown

PR #411: 🧪 Keep the HTTP journal test's teardown off the fetch pool's clock

1 files, +20 / -1

Scope

✅ PR scope looks good.

Structural

✅ No structural bloat detected.

Slop

✅ Slop indicators look low.

Static Analysis

✅ Oxlint found no issues.

Correctness

No extraneous code patterns detected.

@taras
taras merged commit 19bda00 into main Aug 9, 2026
11 checks passed
@taras
taras deleted the guarded-journal-http-teardown branch August 9, 2026 11:59
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