Structural follow-up to #140 (now closed). v0.5.12 shipped option a — a bounded connect + TLS-handshake timeout (connect_timeout_ms, default 10s, upstream karlseguin/websocket.zig#108) — which caps the worst case at ~20s, under StartOS's 60s shutdown grace. This issue tracks the two residual gaps that a timeout alone doesn't close.
Option b: make closeClient() actually abort the in-flight client
RelayConn.closeClient() (src/spider.zig) is a no-op, and active_client is assigned only after Client.init() returns. So stop() -> thread.join() still can't unblock a spider thread mid-init(); it relies entirely on the timeout expiring.
Make closeClient() shut down active_client's socket so stop() can interrupt an in-flight connect/handshake immediately (moving the connect off the shutdown-join critical path), instead of waiting out the timeout. Requires:
- a per-conn mutex, and
- safe lifetime handling —
active_client currently stores a pointer to a stack local in connectAndSubscribe, which is racy as-is.
Residual: DNS resolution is still unbounded
Io.net.HostName.lookup + getOneUncancelable are un-timed and uncancellable, and std.Io.Threaded can't cancel them, so option a does not bound the DNS phase. In practice the libc resolver self-bounds (~10-30s via resolv.conf timeout x attempts x nameservers), usually under grace but not guaranteed. Note: option b does not fix this either — aborting the socket doesn't unblock a thread stuck in getaddrinfo. Bounding DNS robustly needs a DNS watchdog thread (leak-prone) or a cancellable resolver API std doesn't currently provide. Lowest priority; could split into its own issue if picked up.
Context
Structural follow-up to #140 (now closed). v0.5.12 shipped option a — a bounded connect + TLS-handshake timeout (
connect_timeout_ms, default 10s, upstream karlseguin/websocket.zig#108) — which caps the worst case at ~20s, under StartOS's 60s shutdown grace. This issue tracks the two residual gaps that a timeout alone doesn't close.Option b: make
closeClient()actually abort the in-flight clientRelayConn.closeClient()(src/spider.zig) is a no-op, andactive_clientis assigned only afterClient.init()returns. Sostop()->thread.join()still can't unblock a spider thread mid-init(); it relies entirely on the timeout expiring.Make
closeClient()shut downactive_client's socket sostop()can interrupt an in-flight connect/handshake immediately (moving the connect off the shutdown-joincritical path), instead of waiting out the timeout. Requires:active_clientcurrently stores a pointer to a stack local inconnectAndSubscribe, which is racy as-is.Residual: DNS resolution is still unbounded
Io.net.HostName.lookup+getOneUncancelableare un-timed and uncancellable, andstd.Io.Threadedcan't cancel them, so option a does not bound the DNS phase. In practice the libc resolver self-bounds (~10-30s via resolv.conf timeout x attempts x nameservers), usually under grace but not guaranteed. Note: option b does not fix this either — aborting the socket doesn't unblock a thread stuck ingetaddrinfo. Bounding DNS robustly needs a DNS watchdog thread (leak-prone) or a cancellable resolver API std doesn't currently provide. Lowest priority; could split into its own issue if picked up.Context