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:
parent
4dba136c83
commit
78b9580e5f
|
@ -42,6 +42,7 @@ module ActiveSupport
|
|||
autoload :Executor
|
||||
autoload :FileUpdateChecker
|
||||
autoload :EventedFileUpdateChecker
|
||||
autoload :ForkTracker
|
||||
autoload :LogSubscriber
|
||||
autoload :Notifications
|
||||
autoload :Reloader
|
||||
|
|
|
@ -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!
|
|
@ -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
|
Loading…
Reference in New Issue