Skip to content

Add reactor thread workers - #820

Open
andrewfader wants to merge 2 commits into
rails:mainfrom
andrewfader:reactor-thread-workers
Open

andrewfader wants to merge 2 commits into
rails:mainfrom
andrewfader:reactor-thread-workers

Conversation

@andrewfader

@andrewfader andrewfader commented Oct 23, 2026 •

Copy link
Copy Markdown

Thread workers run jobs without an Async reactor, so clients built on Async, like async-http, start one per call and pay for a new connection and TLS handshake every time. Fiber workers (#728) keep a reactor, but run every job on one thread, where a job that blocks without yielding (a C extension decoding an image, CPU-heavy Ruby) stalls the others.

reactor: true on a thread worker keeps the thread pool and gives each thread a reactor that lives as long as the thread, running its jobs one at a time:

workers:
  - queues: [ api_sync, webhooks ]
    threads: 5
    reactor: true

It's a new ReactorThreadPool alongside ThreadPool and FiberPool. Threads start lazily so the pool can be built before forking, a cancelled reactor marks the pool as fatal, and shutdown drains queued executions. It needs the async gem but not fiber-scoped isolation, and database pool sizing is unchanged from threads. reactor with fibers is rejected.

Measured in an app making 10 sequential HTTPS calls per job, 12 jobs across 3 thread workers: per-call reactors opened 120 TLS sessions in 12.5–30.1s; reactor threads opened 3 in 7.9–8.9s.

Pool tests mirroring the fiber pool's, configuration validation, and a reactor_thread entry in the worker execution-mode matrix.

Thread workers run jobs without an Async reactor, so a client built on
Async, like async-http, starts one per call and pays for a new
connection and TLS handshake each time. Fiber workers keep a reactor,
but run every job on one thread, where a job that blocks without
yielding (a C extension decoding an image, CPU-heavy Ruby) stalls the
rest.

`reactor: true` on a thread worker keeps the thread pool and gives each
thread a reactor that lives as long as the thread, running its jobs one
at a time. Clients then reuse their connections across calls and jobs.
Like the fiber pool, threads start lazily so the pool can be built
before forking, and a cancelled reactor marks the pool as fatal. It
requires the async gem but not fiber-scoped isolation, and database
pool sizing is the same as for threads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@andrewfader
andrewfader marked this pull request as ready for review October 23, 2026 03:30
@andrewfader
andrewfader force-pushed the reactor-thread-workers branch from 3cdbf59 to a549ba2 Compare October 24, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant