You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Feat (SSE): replace ld-eventsource with an SDK-owned reconnect loop (qfg-35sm). sdk-ruby was the outlier among the four backend SDKs — sdk-go, sdk-node, and sdk-python all own their reconnect loop, only sdk-ruby handed it off to a library and scraped its log output to observe reconnects. The wire format we actually consume (plain JSON envelopes in single-line data: frames, no named events, no retry directives) is trivial enough that an SDK-owned loop is clearer than the library wrapper. New Quonfig::SSEConfigClient (~520 LoC, lib/quonfig/sse_config_client.rb) handles connect/parse/reconnect end-to-end. restart_total is now incremented at exactly one site under a mutex — verifiable, not log-scraped. ld-eventsource and the transitive http gem are removed from the gemspec. ReconnectCountingLogger and the sse_reconnect_reset_interval option (both 0.0.15-era defensive scaffolding around upstream behavior) are deleted — the bugs they defended against don't exist when the SDK owns the loop. Chaos: 10/10 in a 36-min run (scenarios 02 silent-stall, 05 sse-down-fallback, 09 flapping kill-storm).
Fix (SSE): contain watchdog Thread#raise with Thread.handle_interrupt (qfg-tj18). The new watchdog fires Thread#raise(SSEReadDeadlineExceeded) into the worker on a silent stall — the only reliable cross-platform way to unblock a Ruby thread blocked in Net::HTTP's body-read on macOS. The decision to fire is mutex-guarded against stop(), but the raise itself is delivered at the worker's next interrupt checkpoint, which could be anywhere in the call stack. run_loop's body now runs under Thread.handle_interrupt(SSEReadDeadlineExceeded => :on_blocking) so a late-landing raise can only land inside a blocking call; the read_body block explicitly switches to :immediate. A paranoid backstop rescue outside the until-@stopped loop ensures an escaped raise can never silently kill the worker.
Fix (SSE): isolate on_envelope callback exceptions (qfg-m3lk). A buggy user-supplied listener that raised during envelope delivery used to propagate out of read_body, get caught by run_loop as a transport error, bump restart_total, and reconnect — a perpetual reconnect storm at api-delivery-sse driven by a customer code bug. The callback is now wrapped in begin/rescue StandardError at the invocation site; exceptions are logged with class + message + backtrace sample and the stream continues uninterrupted. Interrupt and SystemExit are deliberately not caught so Ctrl-C still works.
Fix (SSE): classify 401/403/404 as terminal errors (qfg-i5xv). Non-200 responses used to be treated identically — SSEHTTPStatusError raised, run_loop rescued, restart_total bumped, backoff, retry, forever. For a bad SDK key (401) or revoked workspace (403) that was wasted load on api-delivery-sse with no recovery path short of a customer redeploy. New SSEHTTPTerminalError sentinel for 401/403/404; run_loop catches it, invokes on_error, exits the loop without bumping restart_total. Parent Quonfig::Client surfaces a terminal :sse_terminal_failure state distinct from transient :error. 429 and 5xx still retry.
Feat (fork): install Process._fork hook so SSE auto-restarts after fork (qfg-ryov). Ruby threads do not survive fork(2). Customers initializing Quonfig::Client in the Puma master (the preload_app! true / Rails config.eager_load = true convention) used to silently lose SSE in every worker child. New Quonfig::ForkSafety module prepends Process._fork and fans out across all live Quonfig::Client instances (tracked in an ObjectSpace::WeakMap): in the parent before the syscall, threaded components (SSE worker, polling supervisor, telemetry reporter) are torn down; in the child after the syscall, they are rebuilt. @stopped is preserved so a stop()-ed client stays stopped across fork. Covers Process.fork / Kernel#fork; Process.spawn and system("...") exec a new program so in-process state doesn't apply. Ruby 3.0 lacks Process._fork and is documented as requiring manual before_fork / on_worker_boot wiring.