Merge pull request #25302 from schneems/schneems/evented-file-boot-at-check-time-master

EventedFileUpdateChecker boots once per process
This commit is contained in:
Sean Griffin 2016-06-17 10:19:56 -04:00 committed by GitHub
commit 30dd8b2cb0
2 changed files with 87 additions and 6 deletions

View File

@ -3,6 +3,33 @@ require 'pathname'
require 'concurrent/atomic/atomic_boolean'
module ActiveSupport
# Allows you to "listen" to changes in a file system.
# The evented file updater does not hit disk when checking for updates
# instead it uses platform specific file system events to trigger a change
# in state.
#
# The file checker takes an array of files to watch or a hash specifying directories
# and file extensions to watch. It also takes a block that is called when
# EventedFileUpdateChecker#execute is run or when EventedFileUpdateChecker#execute_if_updated
# is run and there have been changes to the file system.
#
# Note: Forking will cause the first call to `updated?` to return `true`.
#
# Example:
#
# checker = EventedFileUpdateChecker.new(["/tmp/foo"], -> { puts "changed" })
# checker.updated?
# # => false
# checker.execute_if_updated
# # => nil
#
# FileUtils.touch("/tmp/foo")
#
# checker.updated?
# # => true
# checker.execute_if_updated
# # => "changed"
#
class EventedFileUpdateChecker #:nodoc: all
def initialize(files, dirs = {}, &block)
@ph = PathHelper.new
@ -13,11 +40,13 @@ module ActiveSupport
@dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) }
end
@block = block
@updated = Concurrent::AtomicBoolean.new(false)
@lcsp = @ph.longest_common_subpath(@dirs.keys)
@block = block
@updated = Concurrent::AtomicBoolean.new(false)
@lcsp = @ph.longest_common_subpath(@dirs.keys)
@pid = Process.pid
@boot_mutex = Mutex.new
if (dtw = directories_to_watch).any?
if (@dtw = directories_to_watch).any?
# Loading listen triggers warnings. These are originated by a legit
# usage of attr_* macros for private attributes, but adds a lot of noise
# to our test suite. Thus, we lazy load it and disable warnings locally.
@ -28,11 +57,18 @@ module ActiveSupport
raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace
end
end
Listen.to(*dtw, &method(:changed)).start
end
boot!
end
def updated?
@boot_mutex.synchronize do
if @pid != Process.pid
boot!
@pid = Process.pid
@updated.make_true
end
end
@updated.true?
end
@ -50,6 +86,9 @@ module ActiveSupport
end
private
def boot!
Listen.to(*@dtw, &method(:changed)).start
end
def changed(modified, added, removed)
unless updated?

View File

@ -11,7 +11,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
end
def new_checker(files = [], dirs = {}, &block)
ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do
ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do |c|
wait
end
end
@ -34,6 +34,48 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase
super
wait
end
test 'notifies forked processes' do
FileUtils.touch(tmpfiles)
checker = new_checker(tmpfiles) { }
assert !checker.updated?
# Pipes used for flow controll across fork.
boot_reader, boot_writer = IO.pipe
touch_reader, touch_writer = IO.pipe
pid = fork do
assert checker.updated?
# Clear previous check value.
checker.execute
assert !checker.updated?
# Fork is booted, ready for file to be touched
# notify parent process.
boot_writer.write("booted")
# Wait for parent process to signal that file
# has been touched.
IO.select([touch_reader])
assert checker.updated?
end
assert pid
# Wait for fork to be booted before touching files.
IO.select([boot_reader])
touch(tmpfiles)
# Notify fork that files have been touched.
touch_writer.write("touched")
assert checker.updated?
Process.wait(pid)
end
end
class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase