diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index fe143c53b3..a3b0160b35 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -42,6 +42,7 @@ module ActiveSupport autoload :Executor autoload :FileUpdateChecker autoload :EventedFileUpdateChecker + autoload :ForkTracker autoload :LogSubscriber autoload :Notifications autoload :Reloader diff --git a/activesupport/lib/active_support/fork_tracker.rb b/activesupport/lib/active_support/fork_tracker.rb new file mode 100644 index 0000000000..f9e112d234 --- /dev/null +++ b/activesupport/lib/active_support/fork_tracker.rb @@ -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! diff --git a/activesupport/test/fork_tracker_test.rb b/activesupport/test/fork_tracker_test.rb new file mode 100644 index 0000000000..07fad11024 --- /dev/null +++ b/activesupport/test/fork_tracker_test.rb @@ -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