Skip to content

Commit

Permalink
Use throw/catch to unwind the stack post fork
Browse files Browse the repository at this point in the history
Fix: #49 (comment)

The fiber solution has the huge drawback on restricting the stack space
to just 4kiB, which is way too small for sizeable applications.

Instead we can use a throw with a callable argument to unwind the stack
and resume execution.
  • Loading branch information
byroot committed Jun 12, 2023
1 parent a2334a8 commit 10ba848
Show file tree
Hide file tree
Showing 3 changed files with 44 additions and 14 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Unreleased

- Use a Fiber rather than a Thread to implement `clean_fork`.
- Preserve the current thread when doing a fork.

# 0.3.0

Expand Down
47 changes: 36 additions & 11 deletions lib/pitchfork.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,17 +121,42 @@ def self.socketpair
end

def self.clean_fork(&block)
# We fork from a fiber to start with a clean stack.
# If we didn't the base stack would grow after each refork
# putting an effective limit on the number of generations.
current_thread = Thread.current
fiber_locals = current_thread.keys.map { |k| [k, current_thread[k]] }

Fiber.new do
# We copy over any fiber local state it might have
fiber_locals.each { |k, v| current_thread[k] = v }
Process.fork(&block)
end.resume
if pid = Process.fork
return pid
end

begin
# Pitchfork recursively refork the worker processes.
# Because of this we need to unwind the stack before resuming execution
# in the child, otherwise on each generation the available stack space would
# get smaller and smaller until it's basically 0.
#
# The very first version of this method used to call fork from a new
# thread, however this can cause issues with some native gems that rely on
# pthread_atfork(3) or pthread_mutex_lock(3), as the new main thread would
# now be different.
#
# A second version used to fork from a new fiber, but fibers have a much smaller
# stack space (https://bugs.ruby-lang.org/issues/3187), so it would break large applications.
#
# The latest version now use `throw` to unwind the stack after the fork, it however
# restrict it to be called only inside `handle_clean_fork`.
if Thread.current[:pitchfork_handle_clean_fork]
throw self, block
else
while block
block = catch(self) do
Thread.current[:pitchfork_handle_clean_fork] = true
block.call
nil
end
end
end
rescue Exception => error
abort
else
exit
end
end

def self.fork_sibling(&block)
Expand Down
9 changes: 7 additions & 2 deletions lib/pitchfork/http_server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -733,10 +733,15 @@ def spawn_mold(current_generation)
mold.start_promotion(@control_socket[1])
mold_loop(mold)
end
true
ensure
rescue
# HACK: we need to call this on error or on no error, but not on throw
# hence why we don't use `ensure`
@promotion_lock.at_fork
raise
else
@promotion_lock.at_fork # We let the spawned mold own the lock
end
true
end

def mold_loop(mold)
Expand Down

0 comments on commit 10ba848

Please sign in to comment.