0.8.1

Patch release for HTTP/3 reload/drain parity, the subinterpreter FD leak fix, shared request-pipeline consolidation, and the drain benchmark profile

Highlights

  • HTTP/3 reload & drain paritygraceful_reload now rotates HTTP/3 (QUIC) workers alongside TCP workers so the H3 generation picks up reimported app code, and HTTP/3 graceful shutdown drains in-flight stream tasks within shutdown_timeoutinstead of hard-cancelling them.
  • Subinterpreter FD leak fixed — subinterpreter workers no longer leak the dup'd listener file descriptor on abnormal stop, and the supervisor reclaims FDs on crash-respawn and after force-stopping a non-draining old generation during reload, so repeated crashes/reloads no longer exhaust the FD budget.
  • Shared request pipeline — the H2/H3 per-request prelude and the sync worker's response paths now route through the shared_request_pipeline and negotiate_compressor_from_meta helpers, restoring RFC 9842 dictionary-compressed zstd (dcz) negotiation on HTTP/2 and HTTP/3. No user-visible behavior change.
  • Drain/reload regression gate — a new reload/drain-under-load benchmark profile drives steady keep-alive load through the realpounce serveCLI, fires SIGHUP then SIGTERM, and emits an artifact-schema JSON that feeds reload/drain health into the regression gate.

Added

  • Reload/drain-under-load benchmark profile (benchmarks/drain_profile.py): drives steady keep-alive /fast + in-flight /slow + /stream load through the real pounce serve CLI, fires SIGHUP then SIGTERM, and emits an artifact-schema JSON whose per-sample drainblock records in-flight completion, the 503/disconnect rate, drain duration, and orphan-worker absence. (#141)

Changed

  • Benchmark hygiene: rewrote the banned PEP 758 parenless multi-except form (except A, B:) in benchmarks/ into the portable parenthesized tuple except (A, B): # fmt: skip, and added benchmarks/to the code-quality scan so the guard now covers it. No runtime behavior change. (#155)
  • Consolidated the H2/H3 per-request prelude (trusted-peer detection, content-encoding negotiation, and access-log filtering) onto the shared_request_pipeline helpers, which also restores the already-advertised RFC 9842 dictionary-compressed zstd (dcz) negotiation on HTTP/2 and HTTP/3. No user-visible behavior change. (#160)
  • Routed the sync worker's SyncApp and ASGI response paths through the sharednegotiate_compressor_from_meta entry point and a local _finalize_response_headershelper, removing the duplicated content-encoding/content-length rewrite blocks. No user-visible behavior change. (#162)
  • Extracted the duplicatedFIRST_COMPLETED race-and-drain logic shared by the disconnect monitor, body reader, and the HTTP/2 and HTTP/1.1 WebSocket bridges into a single pounce._concurrencyhelper that always cancels and awaits the losing task so no task is leaked; behavior is unchanged. (#163)

Fixed

  • Subinterpreter workers no longer leak the dup'd listener file descriptor when they stop abnormally: the bootstrap now closes the reconstructed socket in afinally block (covering a startup-hook/asyncio.start_server/asyncio.run failure before the normal server.close()), and the supervisor records each worker's dup'd FD and reclaims it on crash-respawn and after force-stopping a non-draining old generation during graceful reload, so repeated crashes/reloads no longer exhaust the FD budget. (#106)
  • graceful_reload now rotates HTTP/3 (QUIC) workers alongside TCP workers so the H3 generation also picks up the reimported app — fixing a split-brain where old H3 workers kept serving stale code; each old H3 worker is retired via a per-worker reload event (not the shared shutdown event) and its in-flight streams drain within shutdown_timeoutbefore its UDP transport closes, so HTTP/3 reload is brief-downtime rather than zero-downtime. (#111)
  • HTTP/3 graceful shutdown now drains in-flight stream tasks withinshutdown_timeout instead of hard-cancelling them: existing requests finish (only stragglers past the deadline are aborted), new connections/streams are refused while draining, and CONNECTION_CLOSEis sent after the bounded wait — mirroring the TCP worker's stop-new / bounded-wait / abort-stragglers sequence. (#112)
  • Packaged achirp install extra (pip install bengal-pounce[chirp]) and added bengal-chirp>=0.7.1to the dev dependency-group so the chirp example/compat tests now actually run in CI instead of being skipped. (#150)

Install

uv add "bengal-pounce==0.8.1"