fix: Use Thread.current[] instead of Fiber[] for connection cache#499
Conversation
Fiber[] (inheritable fiber storage) is inherited by reference into every
child thread spawned afterward, so a warmed Net::HTTP socket leaked into
worker threads (Solid Queue, Puma, etc.) and got driven concurrently —
corrupting the stream and surfacing as FrozenError, Errno::EBADF,
IOError ("stream closed in another thread"), and Net::HTTPBadResponse.
Switch to Thread.current[] which is not inherited across threads, giving
each thread its own isolated connection cache.
Closes #496
Original prompt from garen.torikian
|
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
Greptile SummaryFixes a thread-safety bug where
Confidence Score: 5/5Safe to merge — a minimal, well-targeted fix to a real thread-safety bug with two focused regression tests and proper teardown cleanup. The production change is a single line in a private helper. The replacement (Thread.current[]) has well-defined, non-inherited semantics, is safe in both threaded (Puma, Solid Queue) and fiber-based (Async, Falcon) servers, and is correctly documented. Both new tests are deterministic and exercise the isolation property directly. No pre-existing behavior is changed beyond the inheritance semantics that caused the bug. No files require special attention. Important Files Changed
Reviews (2): Last reviewed commit: "Address Greptile review feedback" | Re-trigger Greptile |
- Clarify comment: 'Per-fiber connection cache (non-inherited)' and add note explaining Thread.current[] fiber-local semantics - Stub Net::HTTP#start in connection_for test to avoid real TCP+TLS connections; add @client.shutdown to teardown to close sockets - Swap assert_equal argument order to match Minitest convention (expected, actual)
Description
Fixes #496
The connection cache in
BaseClientusedFiber[](inheritable fiber storage), which is inherited by reference into every child thread spawned afterward. This means a warmedNet::HTTPsocket cached in a parent thread leaked into worker threads (Solid Queue, Puma, etc.) and got driven concurrently — corrupting the stream and surfacing as:FrozenErroronSSLContextErrno::EBADFIOError("stream closed in another thread")Net::HTTPBadResponseFix: Switch from
Fiber[:workos_connections]→Thread.current[:workos_connections].Thread.current[]is not inherited across threads, so each thread gets its own isolated cache.Also corrected the stale "current fiber/thread" comment on
shutdown.Tests added:
test_connection_cache_is_not_shared_with_threads_spawned_after_warming— warms a connection in the parent thread, spawns 4 workers, asserts none inherit the parent's cache or connection (storage-level, deterministic).test_connection_for_opens_a_distinct_connection_per_thread— exercises the realconnection_forpath and asserts each thread opens its own connection.Documentation
Does this require changes to the WorkOS Docs? E.g. the API Reference or code snippets need updates.
No docs changes needed — this is an internal bug fix with no API surface change.
Link to Devin session: https://app.devin.ai/sessions/ab16b6969a864b14ab38e14ccab44e11