Shopify / Shopify/pitchfork

Pitchfork 0.18.2: fork_sibling gets nil PID while spawning Discourse worker on Ruby 3.4

Open
#185 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Ruby
Stars
834
Forks
36
Avg merge
1d 4h
Merged PRs (30d)
7

Description

Description

I'm debugging a worker spawning failure with Pitchfork 0.18.2 in a Discourse development environment running Ruby 3.4.0.

During startup, Pitchfork fails to spawn a service worker. The parent side of fork_sibling receives nil from the pipe instead of the child's PID.

Observed output

DEBUG parent pid_str=nil
mold gen=0 pid=15371 failed to spawn a service, retrying
DEBUG parent pid_str=nil
mold gen=0 pid=15371 failed to spawn a service twice in a row - corrupted mold process?
mold gen=0 pid=15371 reaped (#<Process::Status: pid 15371 exit 1>)

The relevant code is in Pitchfork::HttpServer#fork_sibling.

I temporarily added logging immediately before the yield inside Pitchfork.clean_fork:

pid = Pitchfork.clean_fork(setpgid: setpgid) do
  w.close
  STDERR.puts "DEBUG before yield"
  STDERR.flush
  yield
  STDERR.puts "DEBUG after yield"
end

However, during the failure, DEBUG before yield is never printed.

At the same time, the parent reaches:

pid_str = r.gets

and receives:

DEBUG parent pid_str=nil

This suggests that the middle process exits before the inner clean_fork block reaches the yield, or otherwise closes the pipe without writing the grandchild PID.

Environment

  • Pitchfork: 0.18.2
  • Ruby: 3.4.0
  • Application: Discourse
  • Environment: development
  • Platform: Linux container
  • Pitchfork::REFORKING_AVAILABLE: true
  • UNICORN_WORKERS: 3
  • setpgid: false in Discourse development configuration
  • spawn_timeout: 60 seconds by default

Discourse configuration

The Discourse config/pitchfork.conf.rb defines several fork callbacks, including:

before_fork do |server|
  Discourse.redis.close
  # ...
end

after_mold_fork do |server, mold|
  # ...
  Discourse.redis.close
  Discourse.before_fork
end

after_worker_fork do |server, worker|
  DiscourseEvent.trigger(:web_fork_started)
  Discourse.apply_worker_db_variables_overrides
  Discourse.after_fork
  SignalTrapLogger.instance.after_fork
end

No refork_after configuration is present in the Discourse application configuration.

Additional investigation

REFORKING_AVAILABLE is confirmed to be enabled:

REFORKING_AVAILABLE=true

The Pitchfork source shows that the Linux reforking path uses a double fork:

if middle_pid = FORK_LOCK.synchronize { Process.fork }
  w.close
  process_wait_with_timeout(middle_pid, FORK_TIMEOUT)
  pid_str = r.gets
  # pid_str is nil
else
  reset_signal_handlers
  r.close

  pid = Pitchfork.clean_fork(setpgid: setpgid) do
    w.close
    # DEBUG before yield is never reached
    yield
  end

  w.puts(pid)
  w.close
end

I also tested the basic double-fork + pipe behavior separately in the same environment, and it worked correctly. The parent received the grandchild PID from the pipe.

This makes it less likely that the problem is simply a failure of Process.fork or pipe communication in the environment.

Expected behavior

fork_sibling should successfully create the grandchild worker and write its PID to the pipe so that the parent can read it with:

pid_str = r.gets

Actual behavior

The parent receives nil:

DEBUG parent pid_str=nil

and the mold eventually exits with status 1 after retrying the worker spawn.

Questions

  1. Is there any known compatibility issue between Pitchfork 0.18.2 and Ruby 3.4 involving fork_sibling, clean_fork, or fork callbacks?
  2. Could a callback, thread, or signal handler in the Discourse/Rails process cause the middle process to exit before the clean_fork block reaches yield?
  3. Is there a recommended way to instrument fork_sibling or clean_fork to determine why the middle process exits before w.puts(pid)?
  4. Is there any additional diagnostic information that would be useful to collect?

Thanks!

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with Pitchfork::HttpServer#fork_sibling and Pitchfork.clean_fork, focusing on the Linux double-fork path and the inner block that should write to the pipe. Reproduce with Ruby 3.4 and the Discourse fork callbacks, instrument the middle-process exit and pipe read/write points, and identify why the parent receives no grandchild PID.

Written by the indexing model from the issue text.

Assessment

Tech stack
ruby
Domain
backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.