Skip to content
This repository
Browse code

FileUpdateChecker should be able to handle deleted files.

  • Loading branch information...
commit 80256abb39332dd49996b909d6f0413a15291a90 1 parent 1f5b9bb
José Valim authored December 13, 2011
3  activerecord/lib/active_record/railtie.rb
@@ -95,8 +95,7 @@ class Railtie < Rails::Railtie
95 95
     end
96 96
 
97 97
     initializer "active_record.add_watchable_files" do |app|
98  
-      files = ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"]
99  
-      config.watchable_files.concat files.select { |f| File.exist?(f) }
  98
+      config.watchable_files.concat ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"]
100 99
     end
101 100
 
102 101
     config.after_initialize do
73  activesupport/lib/active_support/file_update_checker.rb
@@ -2,10 +2,27 @@
2 2
 require "active_support/core_ext/array/extract_options"
3 3
 
4 4
 module ActiveSupport
5  
-  # This class is responsible to track files and invoke the given block
6  
-  # whenever one of these files are changed. For example, this class
7  
-  # is used by Rails to reload the I18n framework whenever they are
8  
-  # changed upon a new request.
  5
+  # \FileUpdateChecker specifies the API used by Rails to watch files
  6
+  # and control reloading. The API depends on four methods:
  7
+  #
  8
+  # * +initialize+ which expects two parameters and one block as
  9
+  #   described below;
  10
+  #
  11
+  # * +updated?+ which returns a boolean if there were updates in
  12
+  #   the filesystem or not;
  13
+  #
  14
+  # * +execute+ which executes the given block on initialization
  15
+  #   and updates the counter to the latest timestamp;
  16
+  #
  17
+  # * +execute_if_updated+ which just executes the block if it was updated;
  18
+  #
  19
+  # After initialization, a call to +execute_if_updated+ must execute
  20
+  # the block only if there was really a change in the filesystem.
  21
+  #
  22
+  # == Examples
  23
+  #
  24
+  # This class is used by Rails to reload the I18n framework whenever
  25
+  # they are changed upon a new request.
9 26
   #
10 27
   #   i18n_reloader = ActiveSupport::FileUpdateChecker.new(paths) do
11 28
   #     I18n.reload!
@@ -16,37 +33,38 @@ module ActiveSupport
16 33
   #   end
17 34
   #
18 35
   class FileUpdateChecker
19  
-    # It accepts two parameters on initialization. The first is
20  
-    # the *paths* and the second is *calculate*, a boolean.
21  
-    #
22  
-    # paths must be an array of file paths but can contain a hash as
23  
-    # last argument. The hash must have directories as keys and the
24  
-    # value is an array of extensions to be watched under that directory.
  36
+    # It accepts two parameters on initialization. The first is an array
  37
+    # of files and the second is an optional hash of directories. The hash must
  38
+    # have directories as keys and the value is an array of extensions to be
  39
+    # watched under that directory.
25 40
     #
26  
-    # If *calculate* is true, the latest updated at will calculated
27  
-    # on initialization, therefore, the first call to execute_if_updated
28  
-    # will only evaluate the block if something really changed.
  41
+    # This method must also receive a block that will be called once a path changes.
29 42
     #
30  
-    # This method must also receive a block that will be called once a file changes.
  43
+    # == Implementation details
31 44
     #
32  
-    # This particular implementation checks for added files and updated files,
  45
+    # This particular implementation checks for added and updated files,
33 46
     # but not removed files. Directories lookup are compiled to a glob for
34  
-    # performance. Therefore, while someone can add new files to paths after
35  
-    # initialization, adding new directories is not allowed. Notice that,
36  
-    # depending on the implementation, not even new files may be added.
37  
-    def initialize(paths, calculate=false, &block)
38  
-      @paths = paths
39  
-      @glob  = compile_glob(@paths.extract_options!)
  47
+    # performance. Therefore, while someone can add new files to the +files+
  48
+    # array after initialization (and parts of Rails do depend on this feature),
  49
+    # adding new directories after initialization is not allowed.
  50
+    #
  51
+    # Notice that other objects that implements FileUpdateChecker API may
  52
+    # not even allow new files to be added after initialization. If this
  53
+    # is the case, we recommend freezing the +files+ after initialization to
  54
+    # avoid changes that won't make effect.
  55
+    def initialize(files, dirs={}, &block)
  56
+      @files = files
  57
+      @glob  = compile_glob(dirs)
40 58
       @block = block
41 59
       @updated_at = nil
42  
-      @last_update_at = calculate ? updated_at : nil
  60
+      @last_update_at = updated_at
43 61
     end
44 62
 
45 63
     # Check if any of the entries were updated. If so, the updated_at
46 64
     # value is cached until the block is executed via +execute+ or +execute_if_updated+
47 65
     def updated?
48 66
       current_updated_at = updated_at
49  
-      if @last_update_at != current_updated_at
  67
+      if @last_update_at < current_updated_at
50 68
         @updated_at = updated_at
51 69
         true
52 70
       else
@@ -54,7 +72,7 @@ def updated?
54 72
       end
55 73
     end
56 74
 
57  
-    # Executes the given block expiring any internal cache.
  75
+    # Executes the given block and updates the counter to latest timestamp.
58 76
     def execute
59 77
       @last_update_at = updated_at
60 78
       @block.call
@@ -62,8 +80,7 @@ def execute
62 80
       @updated_at = nil
63 81
     end
64 82
 
65  
-    # Execute the block given if updated. This call
66  
-    # always flush the cache.
  83
+    # Execute the block given if updated.
67 84
     def execute_if_updated
68 85
       if updated?
69 86
         execute
@@ -78,9 +95,9 @@ def execute_if_updated
78 95
     def updated_at #:nodoc:
79 96
       @updated_at || begin
80 97
         all = []
81  
-        all.concat @paths
  98
+        all.concat @files.select { |f| File.exists?(f) }
82 99
         all.concat Dir[@glob] if @glob
83  
-        all.map { |path| File.mtime(path) }.max
  100
+        all.map { |path| File.mtime(path) }.max || Time.at(0)
84 101
       end
85 102
     end
86 103
 
2  activesupport/lib/active_support/i18n_railtie.rb
@@ -64,7 +64,7 @@ def self.initialize_i18n(app)
64 64
       init_fallbacks(fallbacks) if fallbacks && validate_fallbacks(fallbacks)
65 65
 
66 66
       reloader_paths.concat I18n.load_path
67  
-      reloader.execute_if_updated
  67
+      reloader.execute
68 68
 
69 69
       @i18n_inited = true
70 70
     end
36  activesupport/test/file_update_checker_test.rb
@@ -14,7 +14,7 @@ def setup
14 14
 
15 15
   def teardown
16 16
     FileUtils.rm_rf("tmp_watcher")
17  
-    FileUtils.rm(FILES)
  17
+    FileUtils.rm_rf(FILES)
18 18
   end
19 19
 
20 20
   def test_should_not_execute_the_block_if_no_paths_are_given
@@ -24,39 +24,33 @@ def test_should_not_execute_the_block_if_no_paths_are_given
24 24
     assert_equal 0, i
25 25
   end
26 26
 
27  
-  def test_should_invoke_the_block_on_first_call_if_it_does_not_calculate_last_updated_at_on_load
28  
-    i = 0
29  
-    checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
30  
-    checker.execute_if_updated
31  
-    assert_equal 1, i
32  
-  end
33  
-
34  
-  def test_should_not_invoke_the_block_on_first_call_if_it_calculates_last_updated_at_on_load
35  
-    i = 0
36  
-    checker = ActiveSupport::FileUpdateChecker.new(FILES, true){ i += 1 }
37  
-    checker.execute_if_updated
38  
-    assert_equal 0, i
39  
-  end
40  
-
41 27
   def test_should_not_invoke_the_block_if_no_file_has_changed
42 28
     i = 0
43  
-    checker = ActiveSupport::FileUpdateChecker.new(FILES, true){ i += 1 }
  29
+    checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
44 30
     5.times { assert !checker.execute_if_updated }
45 31
     assert_equal 0, i
46 32
   end
47 33
 
48 34
   def test_should_invoke_the_block_if_a_file_has_changed
49 35
     i = 0
50  
-    checker = ActiveSupport::FileUpdateChecker.new(FILES, true){ i += 1 }
  36
+    checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
51 37
     sleep(1)
52 38
     FileUtils.touch(FILES)
53 39
     assert checker.execute_if_updated
54 40
     assert_equal 1, i
55 41
   end
56 42
 
57  
-  def test_should_cache_updated_result_until_flushed
  43
+  def test_should_be_robust_enough_to_handle_deleted_files
58 44
     i = 0
59  
-    checker = ActiveSupport::FileUpdateChecker.new(FILES, true){ i += 1 }
  45
+    checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
  46
+    FileUtils.rm(FILES)
  47
+    assert !checker.execute_if_updated
  48
+    assert_equal 0, i
  49
+  end
  50
+
  51
+  def test_should_cache_updated_result_until_execute
  52
+    i = 0
  53
+    checker = ActiveSupport::FileUpdateChecker.new(FILES){ i += 1 }
60 54
     assert !checker.updated?
61 55
 
62 56
     sleep(1)
@@ -69,7 +63,7 @@ def test_should_cache_updated_result_until_flushed
69 63
 
70 64
   def test_should_invoke_the_block_if_a_watched_dir_changed_its_glob
71 65
     i = 0
72  
-    checker = ActiveSupport::FileUpdateChecker.new([{"tmp_watcher" => [:txt]}], true){ i += 1 }
  66
+    checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => [:txt]){ i += 1 }
73 67
     FileUtils.cd "tmp_watcher" do
74 68
       FileUtils.touch(FILES)
75 69
     end
@@ -79,7 +73,7 @@ def test_should_invoke_the_block_if_a_watched_dir_changed_its_glob
79 73
 
80 74
   def test_should_not_invoke_the_block_if_a_watched_dir_changed_its_glob
81 75
     i = 0
82  
-    checker = ActiveSupport::FileUpdateChecker.new([{"tmp_watcher" => :rb}], true){ i += 1 }
  76
+    checker = ActiveSupport::FileUpdateChecker.new([], "tmp_watcher" => :rb){ i += 1 }
83 77
     FileUtils.cd "tmp_watcher" do
84 78
       FileUtils.touch(FILES)
85 79
     end
2  railties/lib/rails/application.rb
@@ -124,7 +124,7 @@ def watchable_args
124 124
         dirs[path.to_s] = [:rb]
125 125
       end
126 126
 
127  
-      files << dirs
  127
+      [files, dirs]
128 128
     end
129 129
 
130 130
     # Initialize the application passing the given group. By default, the
7  railties/lib/rails/application/finisher.rb
@@ -64,10 +64,9 @@ module Finisher
64 64
       # routes added in the hook are still loaded.
65 65
       initializer :set_routes_reloader_hook do
66 66
         reloader = routes_reloader
67  
-        hook = lambda { reloader.execute_if_updated }
68  
-        hook.call
  67
+        reloader.execute_if_updated
69 68
         self.reloaders << reloader
70  
-        ActionDispatch::Reloader.to_prepare(&hook)
  69
+        ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated }
71 70
       end
72 71
 
73 72
       # Set app reload just after the finisher hook to ensure
@@ -79,7 +78,7 @@ module Finisher
79 78
         end
80 79
 
81 80
         if config.reload_classes_only_on_change
82  
-          reloader = config.file_watcher.new(watchable_args, true, &callback)
  81
+          reloader = config.file_watcher.new(*watchable_args, &callback)
83 82
           self.reloaders << reloader
84 83
           # We need to set a to_prepare callback regardless of the reloader result, i.e.
85 84
           # models should be reloaded if any of the reloaders (i18n, routes) were updated.
15  railties/lib/rails/application/routes_reloader.rb
@@ -4,11 +4,10 @@ module Rails
4 4
   class Application
5 5
     class RoutesReloader
6 6
       attr_reader :route_sets, :paths
7  
-      delegate :execute_if_updated, :updated?, :to => :@updater
  7
+      delegate :execute_if_updated, :execute, :updated?, :to => :updater
8 8
 
9  
-      def initialize(updater=ActiveSupport::FileUpdateChecker)
  9
+      def initialize
10 10
         @paths      = []
11  
-        @updater    = updater.new(paths) { reload! }
12 11
         @route_sets = []
13 12
       end
14 13
 
@@ -20,7 +19,15 @@ def reload!
20 19
         revert
21 20
       end
22 21
 
23  
-    protected
  22
+    private
  23
+
  24
+      def updater
  25
+        @updater ||= begin
  26
+          updater = ActiveSupport::FileUpdateChecker.new(paths) { reload! }
  27
+          updater.execute
  28
+          updater
  29
+        end
  30
+      end
24 31
 
25 32
       def clear!
26 33
         route_sets.each do |routes|
32  railties/test/application/loading_test.rb
@@ -175,6 +175,38 @@ def self.counter; 2; end
175 175
     assert_equal "1", last_response.body
176 176
   end
177 177
 
  178
+  test "added files also trigger reloading" do
  179
+    add_to_config <<-RUBY
  180
+      config.cache_classes = false
  181
+    RUBY
  182
+
  183
+    app_file 'config/routes.rb', <<-RUBY
  184
+      $counter = 0
  185
+      AppTemplate::Application.routes.draw do
  186
+        match '/c', :to => lambda { |env| User; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] }
  187
+      end
  188
+    RUBY
  189
+
  190
+    app_file "app/models/user.rb", <<-MODEL
  191
+      class User
  192
+        $counter += 1
  193
+      end
  194
+    MODEL
  195
+
  196
+    require 'rack/test'
  197
+    extend Rack::Test::Methods
  198
+
  199
+    require "#{rails_root}/config/environment"
  200
+
  201
+    get "/c"
  202
+    assert_equal "1", last_response.body
  203
+
  204
+    app_file "db/schema.rb", ""
  205
+
  206
+    get "/c"
  207
+    assert_equal "2", last_response.body
  208
+  end
  209
+
178 210
   protected
179 211
 
180 212
   def setup_ar!

0 notes on commit 80256ab

Jeff Rafter

This change is not backwards compatible for the defaults used by most engines (e.g. forem, others). Can we at least get a deprecation notice? I can work up a sample if we need a failing test.

José Valim

Sorry, but I don't understand exactly what do you mean. This class is pretty much internal to Rails. The documentation is only for people that wishes to implement their own file updater. In other words, this implementation is internal to Rails, but the contract used by the reloader should be stable from 3.2 on. Maybe we should add such a note to this class docs?

Jeff Rafter
José Valim

Which API from the file update checker are you using in your engines? What do you want to achieve? We should have a public API for whatever you want to do (via Rails.application or Rails::Engine) instead of changing the internals. FileUpdateChecker is an implementation detail.

Please sign in to comment.
Something went wrong with that request. Please try again.