Improve reliability of EventedFileUpdateCheckerTest fork test

`Listen.to` starts a bunch of background threads that need to perform
some work before they are able to receive events, but it doesn't block
until they are ready, which expose us to a race condition.

With `wait_for_state(:processing_events)` we can ensure that it's ready on
Linux, however on macOS, the Darwin backend has a second background thread
we can't wait on.

As a workaround we wait a bit after the fork to allow that thread to
reach it's listning state.
This commit is contained in:
Jean Boussier 2023-03-24 13:11:15 +01:00
parent dd722263ff
commit c650ef4230
2 changed files with 21 additions and 4 deletions

@ -45,6 +45,10 @@ def initialize(files, dirs = {}, &block)
ObjectSpace.define_finalizer(self, @core.finalizer)
end
def inspect
"#<ActiveSupport::EventedFileUpdateChecker:#{object_id} @files=#{@core.files.to_a.inspect}"
end
def updated?
if @core.restart?
@core.thread_safely(&:restart)
@ -68,7 +72,7 @@ def execute_if_updated
end
class Core
attr_reader :updated
attr_reader :updated, :files
def initialize(files, dirs)
@files = files.map { |file| Pathname(file).expand_path }.to_set
@ -86,7 +90,11 @@ def initialize(files, dirs)
@mutex = Mutex.new
start
@after_fork = ActiveSupport::ForkTracker.after_fork { start }
# inotify / FSEvents file descriptors are inherited on fork, so
# we need to reopen them otherwise only the parent or the child
# will be notified.
# FIXME: this callback is keeping a reference on the instance
@after_fork = ActiveSupport::ForkTracker.after_fork { restart }
end
def finalizer
@ -107,6 +115,11 @@ def start
@dtw, @missing = [*@dtw, *@missing].partition(&:exist?)
@listener = @dtw.any? ? Listen.to(*@dtw, &method(:changed)) : nil
@listener&.start
# Wait for the listener to be ready to avoid race conditions
# Unfortunately this isn't quite enough on macOS because the Darwin backend
# has an extra private thread we can't wait on.
@listener&.wait_for_state(:processing_events)
end
def stop

@ -26,7 +26,7 @@ def teardown
end
def wait
sleep 1
sleep 0.5
end
def mkdir(dirs)
@ -60,6 +60,10 @@ def rm_f(files)
pid = fork do
assert_not_predicate checker, :updated?
# The listen gem start multiple background threads that need to reach a ready state.
# Unfortunately, it doesn't look like there is a clean way to block until they are ready.
wait
# Fork is booted, ready for file to be touched
# notify parent process.
boot_writer.write("booted")
@ -70,7 +74,7 @@ def rm_f(files)
assert_predicate checker, :updated?
rescue Exception => ex
result_writer.write(ex.class.name)
result_writer.write("#{ex.class.name}: #{ex.message}")
raise
ensure
result_writer.close