Skip to content

Commit

Permalink
Merge pull request rails#25302 from schneems/schneems/evented-file-bo…
Browse files Browse the repository at this point in the history
…ot-at-check-time-master

EventedFileUpdateChecker boots once per process
  • Loading branch information
sgrif authored Jun 17, 2016
2 parents 58b70b4 + 844af9f commit 30dd8b2
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 6 deletions.
49 changes: 44 additions & 5 deletions activesupport/lib/active_support/evented_file_update_checker.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,33 @@
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
Expand All @@ -13,11 +40,13 @@ def initialize(files, dirs = {}, &block)
@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.
Expand All @@ -28,11 +57,18 @@ def initialize(files, dirs = {}, &block)
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

Expand All @@ -50,6 +86,9 @@ def execute_if_updated
end

private
def boot!
Listen.to(*@dtw, &method(:changed)).start
end

def changed(modified, added, removed)
unless updated?
Expand Down
44 changes: 43 additions & 1 deletion activesupport/test/evented_file_update_checker_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def setup
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
Expand All @@ -34,6 +34,48 @@ def rm_f(files)
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
Expand Down

0 comments on commit 30dd8b2

Please sign in to comment.