Skip to content

fix: Use Thread.current[] instead of Fiber[] for connection cache#499

Merged
gjtorikian merged 2 commits into
mainfrom
devin/1781620963-fix-thread-local-connection-cache
Jun 16, 2026
Merged

fix: Use Thread.current[] instead of Fiber[] for connection cache#499
gjtorikian merged 2 commits into
mainfrom
devin/1781620963-fix-thread-local-connection-cache

Conversation

@devin-ai-integration

Copy link
Copy Markdown
Contributor

Description

Fixes #496

The connection cache in BaseClient used Fiber[] (inheritable fiber storage), which is inherited by reference into every child thread spawned afterward. This means a warmed Net::HTTP socket cached in a parent thread leaked into worker threads (Solid Queue, Puma, etc.) and got driven concurrently — corrupting the stream and surfacing as:

  • FrozenError on SSLContext
  • Errno::EBADF
  • IOError ("stream closed in another thread")
  • Net::HTTPBadResponse

Fix: 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:

  1. 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).
  2. test_connection_for_opens_a_distinct_connection_per_thread — exercises the real connection_for path 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.

[ ] Yes

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

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
@devin-ai-integration devin-ai-integration Bot requested review from a team as code owners June 16, 2026 14:49
@devin-ai-integration

Copy link
Copy Markdown
Contributor Author
Original prompt from garen.torikian

as yourself, apply this patch to a branch in workos/workos-ruby and open a PR. we are aiming to close #496

What changed

lib/workos/base_client.rb — the one-line root-cause fix plus explanatory comments:

  •  Fiber[:workos_connections] ||= {}
    
  •  Thread.current[:workos_connections] ||= {}
    

Fiber[] is inheritable fiber storage — child threads inherit it by reference, so a warmed Net::HTTP socket leaked into every worker thread spawned afterward. Thread.current[] is not inherited, giving
each thread its own isolated cache. (Also corrected the stale "current fiber/thread" comment on shutdown.)

test/workos/test_base_client.rb — two tests:

  1. 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.
  2. test_connection_for_opens_a_distinct_connection_per_thread — drives the real connection_for path and asserts each thread opens its own connection.

Plus a teardown (calling super) that clears the thread-local cache between tests.
ATTACHMENT:"https://app.devin.ai/attachments/392840c5-0a48-46e9-a205-0187917d32f4/my-changes.patch"

@devin-ai-integration

Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@devin-ai-integration devin-ai-integration Bot changed the title Fix thread-safety of per-thread connection cache fix: Use Thread.current[] instead of Fiber[] for connection cache Jun 16, 2026
@greptile-apps

greptile-apps Bot commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Fixes a thread-safety bug where Fiber[:workos_connections] (inheritable fiber storage) was shared by reference with every child thread spawned after a connection was warmed, causing concurrent Net::HTTP socket access and the associated stream-corruption errors. The fix replaces it with Thread.current[:workos_connections], which is not inherited across threads.

  • lib/workos/base_client.rb: One-line change in thread_connections; accompanied by a detailed comment explaining the Fiber[] vs Thread.current[] semantics and the fiber-server (Async/Falcon) behavior.
  • test/workos/test_base_client.rb: Adds a teardown that shuts down connections properly, a deterministic storage-isolation test using FakeConnection, and a connection_for path test with Net::HTTP#start stubbed via define_method/UnboundMethod to avoid live sockets.

Confidence Score: 5/5

Safe 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

Filename Overview
lib/workos/base_client.rb Single-line fix switching Fiber[:workos_connections] to Thread.current[:workos_connections]; added a thorough explanatory comment covering the inheritance semantics and the fiber-server trade-off
test/workos/test_base_client.rb Adds teardown cleanup (shutdown + nil cache), a deterministic storage-level isolation test, and a connection_for test with Net::HTTP#start stubbed via define_method/UnboundMethod; both tests correctly verify per-thread isolation

Reviews (2): Last reviewed commit: "Address Greptile review feedback" | Re-trigger Greptile

Comment thread test/workos/test_base_client.rb
Comment thread test/workos/test_base_client.rb Outdated
Comment thread lib/workos/base_client.rb Outdated
- 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)
@gjtorikian gjtorikian merged commit a44d650 into main Jun 16, 2026
7 checks passed
@gjtorikian gjtorikian deleted the devin/1781620963-fix-thread-local-connection-cache branch June 16, 2026 22:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Lots of networking errors from from workos/base_client.rb:140:in 'block in WorkOS::BaseClient#execute_request after upgrading the gem

1 participant