Skip to content

Configure parallel tests in rails app (which doesn't work - yet)#78

Open
zcotter wants to merge 2 commits intotemporalio:mainfrom
zcotter:zc-parallelize-example
Open

Configure parallel tests in rails app (which doesn't work - yet)#78
zcotter wants to merge 2 commits intotemporalio:mainfrom
zcotter:zc-parallelize-example

Conversation

@zcotter
Copy link
Copy Markdown

@zcotter zcotter commented Mar 31, 2026

Hello! Our apps use parallel tests in rails to make the test suite finish faster. See rails docs here: https://api.rubyonrails.org/classes/ActiveSupport/TestCase.html#method-c-parallelize

However, it seems this is not compatible with the temporal test_helper.rb setup documented in this repo. See error output below when I run tests with the test suite configured to run tests in parallel (they pass without my code change so I know its not an environment issue, etc). How can we get temporal running along with parallel tests?

Thanks for your help!

$ bin/rails test
Running 5 tests in parallel using 10 processes
Run options: --seed 1715

# Running:


thread '<unnamed>' (16005) panicked at /home/runner/work/sdk-ruby/sdk-ruby/temporalio/tmp/cargo-vendor/tokio/src/runtime/io/driver.rs:260:27:
failed to wake I/O driver: Os { code: 9, kind: Uncategorized, message: "Bad file descriptor" }
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
/Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/parallelization/worker.rb:15: [BUG] failed to wake I/O driver: Os { code: 9, kind: Uncategorized, message: "Bad file descriptor" }
ruby 3.4.9 (2026-03-11 revision 76cca827ab) +YJIT +PRISM [arm64-darwin25]

-- Crash Report log information --------------------------------------------
   See Crash Report log file in one of the following locations:
     * ~/Library/Logs/DiagnosticReports
     * /Library/Logs/DiagnosticReports
   for more details.
Don't forget to include the above Crash Report log file in bug reports.

-- Control frame information -----------------------------------------------
c:0012 p:---- s:0053 e:000052 CFUNC  :fork
c:0011 p:0004 s:0049 e:000048 METHOD /Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/paralleli
c:0010 p:0011 s:0045 e:000044 BLOCK  /Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/paralleli [FINISH]
c:0009 p:---- s:0041 e:000040 IFUNC
c:0008 p:0024 s:0038 e:000036 METHOD <internal:numeric>:257 [FINISH]
c:0007 p:---- s:0032 e:000031 CFUNC  :each
c:0006 p:---- s:0029 e:000028 CFUNC  :map
c:0005 p:0008 s:0025 e:000024 METHOD /Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/paralleli
c:0004 p:0023 s:0021 e:000020 METHOD /Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/paralleli
c:0003 p:0128 s:0017 e:000016 METHOD /Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/minitest-5.27.0/lib/minitest.rb:298
c:0002 p:0045 s:0008 E:001290 BLOCK  /Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/minitest-5.27.0/lib/minitest.rb:85 [FINISH]
c:0001 p:0000 s:0003 E:001470 DUMMY  [FINISH]

-- Ruby level backtrace information ----------------------------------------
/Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/minitest-5.27.0/lib/minitest.rb:85:in 'block in autorun'
/Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/minitest-5.27.0/lib/minitest.rb:298:in 'run'
/Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/parallelize_executor.rb:19:in 'start'
/Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/parallelization.rb:36:in 'start'
/Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/parallelization.rb:36:in 'map'
/Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/parallelization.rb:36:in 'each'
<internal:numeric>:257:in 'times'
/Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/parallelization.rb:37:in 'block in start'
/Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/parallelization/worker.rb:15:in 'start'
/Users/zachcotter/.asdf/installs/ruby/3.4.9/lib/ruby/gems/3.4.0/gems/activesupport-8.0.4.1/lib/active_support/testing/parallelization/worker.rb:15:in 'fork'

-- Threading information ---------------------------------------------------
Total ractor count: 1
Ruby thread count for this ractor: 1

@zcotter zcotter requested a review from a team as a code owner March 31, 2026 22:22
@CLAassistant
Copy link
Copy Markdown

CLA assistant check
Thank you for your submission! We really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

chris-olszewski added a commit to temporalio/sdk-ruby that referenced this pull request Apr 1, 2026
Reproduces temporalio/samples-ruby#78: when a process forks after a
Temporal runtime is created (e.g., Rails parallelize) and the child
exits with `exit` (triggering Ruby's object finalization), the Rust
Drop for CoreRuntime tries to shut down Tokio, which wakes the I/O
driver using inherited (now invalid) kqueue/epoll FDs — causing a
panic: "failed to wake I/O driver: Bad file descriptor".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
fixtures :all

parallelize(workers: :number_of_processors, threshold: 1)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't work due to us not supporting using native backed Ruby structures across forks: https://ruby.temporal.io/#forking

This can be worked around by setting up a Temporal::Client for each process.

Suggested change
parallelize(workers: :number_of_processors, threshold: 1)
parallelize(workers: :number_of_processors, threshold: 1)
# Create a fresh runtime and client in each forked worker,
# connecting to the same dev server started in the parent.
parallelize_setup do |_worker|
TemporalClient.instance = Temporalio::Client.connect(
TemporalClient.server_target,
'default',
runtime: Temporalio::Runtime.new,
logger: Rails.logger
)
end

Note, this requires adding server_target getter/setter on the TemporalClient module in this sample.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick response @chris-olszewski ! I tried your sample code with parallelize_setup and I'm still encountering a few other issues:

  1. RuntimeError: Client already set
  2. Temporalio::Internal::Bridge::Error: Cannot create worker across forks (original runtime PID is 74216, current is 74376)
  3. The test suite freezes before finishing until i kill it

I pushed up the changes I made in case I got lost on the server_target implementation. Appreciate your help!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, I apologize. I accidentally was using a build of the gem where I was leaking all of the native owned resources (which avoids the issue in a very dumb way).

The Temporalio::Testing::WorkflowEnvironment creates a new Runtime which can't be shared across processes, so that in addition to the client need to be setup per process.
e.g.

    parallelize_setup do |_worker|
      @temporal_test_env = Temporalio::Testing::WorkflowEnvironment.start_local(logger: Rails.logger)
      TemporalClient.instance = @temporal_test_env.client
    end
    parallelize_teardown do |_worker|
      @temporal_test_env&.shutdown
    end

And remove the module level test server start (and the temporal_client.rs changes are no longer necessary).

I realize this isn't great as you're starting a dev server per-process, but it should prevent sharing any native backed objects across forks.

chris-olszewski added a commit to temporalio/sdk-ruby that referenced this pull request Apr 1, 2026
Reproduces temporalio/samples-ruby#78: when a process forks after a
Temporal runtime is created (e.g., Rails parallelize) and the child
exits with `exit` (triggering Ruby's object finalization), the Rust
Drop for CoreRuntime tries to shut down Tokio, which wakes the I/O
driver using inherited (now invalid) kqueue/epoll FDs — causing a
panic: "failed to wake I/O driver: Bad file descriptor".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

squash
chris-olszewski added a commit to temporalio/sdk-ruby that referenced this pull request Apr 1, 2026
* Add failing test for fork + process exit Tokio panic

Reproduces temporalio/samples-ruby#78: when a process forks after a
Temporal runtime is created (e.g., Rails parallelize) and the child
exits with `exit` (triggering Ruby's object finalization), the Rust
Drop for CoreRuntime tries to shut down Tokio, which wakes the I/O
driver using inherited (now invalid) kqueue/epoll FDs — causing a
panic: "failed to wake I/O driver: Bad file descriptor".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

squash

* fix: avoid cleaning up tokio in forked children

* empty to retrigger cla
@zcotter zcotter requested a review from chris-olszewski April 1, 2026 18:32
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.

3 participants