Skip to content

CWIST v3.7.2

Choose a tag to compare

@gg582 gg582 released this 30 Sep 01:14
· 669 commits to main since this release

Cut from main at 92a0866d, with every CI workflow green on that commit and the source archive built and tested clean without Git metadata.

This release is not bug-fix-only. Besides the fixes below it carries the experimental Rust bindings step from #287. Treat the Rust API as alpha: it may change before v4.0.

Included experimental features (alpha)

  • Rust App::listen and cwist::shutdown (issue #36, PR #287, community contribution): serving a Rust app on a port with graceful shutdown. Adds three small, additive C lifecycle hooks: cwist_app_listen_ex() (explicit worker/server-mode overrides), cwist_http_pool_init_mode(), and the public cwist_shutdown_request(). Existing cwist_app_listen() behaviour is unchanged.

Fixes and improvements

1. TLS handshake shepherd was O(n) per event: 35% of handshakes failed at 20k connects/s

perf(https): keep the handshake shepherd's pending list O(1) per event (PR #289)

The shepherd kept pending handshakes in a singly linked list: every epoll event scanned the list to unlink an entry, and every wait round walked it all for expiry. Under a connect burst a shard held tens of thousands of entries and queued handshakes ran out their 45 s budget. The list is now doubly linked (O(1) unlink) and kept in deadline order (expiry sweep stops at the first live entry). Measured at 1,000,000 connections opened at 20,000/s: served 538,051 -> 1,000,000, handshake failures 351,821 -> 0.

2. Every worker's first request was ~10 ms late

perf(app): prime libttak TSC calibration before forking workers (PR #290)

libttak's TSC calibration (nanosleep(10ms)) ran lazily inside the first request path of every forked worker. The supervisor now primes it once before the fork loop; children inherit the calibrated value copy-on-write. Measured with 12 workers: slow first requests 11 of 24 -> 0 of 24.

3. Churned TLS connections closed with RST instead of FIN

fix(https): drain the receive queue before closing TLS connections (PR #291)

https_connection_teardown() closed right after SSL_shutdown(), but the peer's close_notify (or a pipelined request) routinely sat unread in the receive queue, so the kernel answered with RST — flushing response bytes the client had not read. The teardown now drains the queue (bounded, non-blocking) before close(). Measured over ~35k churned connections: TCPAbortOnData 24,256 -> 800 (the remainder are client-side aborts a server cannot prevent).

4. Same RST teardown on the async plain-HTTP path

fix(http): drain the receive queue in cwist_http_async_close (PR #292)

Mirrors the TLS drain for the C1M plain-HTTP close helper.

5. Deferred responses: flush failure handed a dangling connection to the completion (use-after-free)

fix(app): hand deferred flush-failure teardown to the async completion

When a handler deferred via cwist_async_defer and earlier responses in the same turn left bytes in the coalesce stash, a flush failure made the batch loop close the connection — but req/res ownership had already moved to the cwist_async, which later wrote through the freed conn shell and possibly-reused fd (use-after-free, double release, response bytes landing on an unrelated connection). The stash now drains before the defer is acked, and on failure the teardown goes through cwist_async_abort() so the completion path owns the close exactly once.

6. Idle keep-alive connections held 32 KB each

perf(http): shrink idle keep-alive stashes to a 4 KiB floor

A served keep-alive connection kept its 16 KiB receive stash plus 16 KiB coalesce buffer for its whole idle lifetime (~24 KB RSS per connection; 1M idle connections measured at 22.9 GiB). At rearm both stashes now shrink to a 4 KiB floor; grow-on-demand restores capacity on the next busy turn, so no per-request alloc/free churn is reintroduced.

7. Shutdown always sat out a fixed 5-second drain, dead time

fix(shutdown): exit the connection drain early when nothing is left

The post-stop drain was an unconditional sleep(5) placed after the reactor and pool were already torn down. It now polls the live-connection counter in 100 ms slices and exits as soon as nothing remains, and honors a new CWIST_DRAIN_TIMEOUT env override (0..3600 s, default unchanged at 5).

8. Slowloris: classic-pool header read had no deadline

fix(http): bound the classic-pool header read

cwist_http_receive_request() recv()d the header block on a blocking socket with no timeout, so a client dribbling the header byte-by-byte held a pool thread indefinitely. The read is now bounded by the same total budget the TLS path enforces (CWIST_HTTP_HEADERS_TIMEOUT_MS, default 120 s).

9. Idle reaper closed keep-alive sockets with RST

fix(http): drain before idle-reaper close

The keep-alive idle reaper closed the fd bare; a request pipelined into the post-timeout, pre-dispatch window was discarded with an RST. It now drains through the same graceful-teardown path as every other close.

10. WASI 0.2 smoke link broke

fix(wasi): restore the server-subsystem stubs dropped from compat.c

The deferred-teardown fix (5) made async.c's completion path survive link-time garbage collection on the WASI target, exposing references the WASM stubs no longer covered. Restored the full stub set; make wasip2-smoke passes again.

11. Latency-probe watchdog gave no diagnostics on a rare stall

test(reactor): make the latency-probe watchdog report where it stopped

A rare test_latency_probe stall in CI killed the process with a bare "Alarm clock". A SIGALRM handler now prints how far the test got before exiting with the same code, distinguishing a lost completion from a loaded runner.

API note (breaking, internal surface)

cwist_http_async_close was removed from the public header and is now an internal, engine-owned teardown. It had no callers outside the framework; handlers still signal teardown by returning CWIST_ASYNC_CLOSE, and deferred completions already own rearm/close. This closes the arbitrary-thread-close fd-reuse race the public symbol invited.

New knobs

  • CWIST_DRAIN_TIMEOUT: shutdown drain upper bound in seconds (default 5).
  • Existing TLS knobs unchanged: CWIST_HTTPS_IDLE_TIMEOUT_MS, CWIST_HTTPS_PARK=0, CWIST_HTTPS_HANDSHAKE_TIMEOUT_MS.

Tests

New pool-path coverage for the v3.7.1 TLS changes (PR #288, test_https_park): idle TLS parking (HTTP/1.1 and HTTP/2, including expiry), HTTP/2 deferred responses over the pool, and content-length after compression.