Introduce a callback for forked children

We hook into Kernel#fork and Process#fork, when they are invoked
we can trigger all the registered callbacks.

There is also a `check!` method that can be called to very cheaply
detect if the process was forked.
This commit is contained in:
Jean Boussier 2019-09-27 13:20:48 +02:00
parent 4dba136c83
commit 78b9580e5f
3 changed files with 225 additions and 0 deletions

View File

@ -42,6 +42,7 @@ module ActiveSupport
autoload :Executor
autoload :FileUpdateChecker
autoload :EventedFileUpdateChecker
autoload :ForkTracker
autoload :LogSubscriber
autoload :Notifications
autoload :Reloader

View File

@ -0,0 +1,50 @@
# frozen_string_literal: true
module ActiveSupport
module ForkTracker # :nodoc:
module CoreExt
def fork(*)
if block_given?
super do
ForkTracker.check!
yield
end
else
unless pid = super
ForkTracker.check!
end
pid
end
end
end
@pid = Process.pid
@callbacks = []
class << self
def check!
if @pid != Process.pid
@callbacks.each(&:call)
@pid = Process.pid
end
end
def hook!
::Object.prepend(CoreExt)
::Kernel.singleton_class.prepend(CoreExt)
::Process.singleton_class.prepend(CoreExt)
end
def after_fork(&block)
@callbacks << block
block
end
def unregister(callback)
@callbacks.delete(callback)
end
end
end
end
ActiveSupport::ForkTracker.hook!

View File

@ -0,0 +1,174 @@
# frozen_string_literal: true
require "abstract_unit"
class ForkTrackerTest < ActiveSupport::TestCase
def test_object_fork
read, write = IO.pipe
called = false
handler = ActiveSupport::ForkTracker.after_fork do
called = true
write.write "forked"
end
pid = fork do
read.close
write.close
exit!
end
write.close
Process.waitpid(pid)
assert_equal "forked", read.read
read.close
assert_not called
ensure
ActiveSupport::ForkTracker.unregister(handler)
end
def test_object_fork_without_block
read, write = IO.pipe
called = false
handler = ActiveSupport::ForkTracker.after_fork do
called = true
write.write "forked"
end
if pid = fork
write.close
Process.waitpid(pid)
assert_equal "forked", read.read
read.close
assert_not called
else
read.close
write.close
exit!
end
ensure
ActiveSupport::ForkTracker.unregister(handler)
end
def test_process_fork
read, write = IO.pipe
called = false
handler = ActiveSupport::ForkTracker.after_fork do
called = true
write.write "forked"
end
pid = Process.fork do
read.close
write.close
exit!
end
write.close
Process.waitpid(pid)
assert_equal "forked", read.read
read.close
assert_not called
ensure
ActiveSupport::ForkTracker.unregister(handler)
end
def test_process_fork_without_block
read, write = IO.pipe
called = false
handler = ActiveSupport::ForkTracker.after_fork do
called = true
write.write "forked"
end
if pid = Process.fork
write.close
Process.waitpid(pid)
assert_equal "forked", read.read
read.close
assert_not called
else
read.close
write.close
exit!
end
ensure
ActiveSupport::ForkTracker.unregister(handler)
end
def test_kernel_fork
read, write = IO.pipe
called = false
handler = ActiveSupport::ForkTracker.after_fork do
called = true
write.write "forked"
end
pid = Kernel.fork do
read.close
write.close
exit!
end
write.close
Process.waitpid(pid)
assert_equal "forked", read.read
read.close
assert_not called
ensure
ActiveSupport::ForkTracker.unregister(handler)
end
def test_kernel_fork_without_block
read, write = IO.pipe
called = false
handler = ActiveSupport::ForkTracker.after_fork do
called = true
write.write "forked"
end
if pid = Kernel.fork
write.close
Process.waitpid(pid)
assert_equal "forked", read.read
read.close
assert_not called
else
read.close
write.close
exit!
end
ensure
ActiveSupport::ForkTracker.unregister(handler)
end
def test_check
count = 0
handler = ActiveSupport::ForkTracker.after_fork { count += 1 }
assert_no_difference -> { count } do
3.times { ActiveSupport::ForkTracker.check! }
end
Process.stub(:pid, Process.pid + 1) do
assert_difference -> { count }, +1 do
3.times { ActiveSupport::ForkTracker.check! }
end
end
assert_difference -> { count }, +1 do
3.times { ActiveSupport::ForkTracker.check! }
end
ensure
ActiveSupport::ForkTracker.unregister(handler)
end
end