Pitchfork 0.18.2: fork_sibling gets nil PID while spawning Discourse worker on Ruby 3.4
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:trueUNICORN_WORKERS:3setpgid:falsein Discourse development configurationspawn_timeout:60seconds 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
- Is there any known compatibility issue between Pitchfork 0.18.2 and Ruby 3.4 involving
fork_sibling,clean_fork, or fork callbacks? - Could a callback, thread, or signal handler in the Discourse/Rails process cause the middle process to exit before the
clean_forkblock reachesyield? - Is there a recommended way to instrument
fork_siblingorclean_forkto determine why the middle process exits beforew.puts(pid)? - Is there any additional diagnostic information that would be useful to collect?
Thanks!
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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