diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb index bd24256427630..3411bd14ea24f 100644 --- a/actionpack/test/dispatch/reloader_test.rb +++ b/actionpack/test/dispatch/reloader_test.rb @@ -129,6 +129,15 @@ def test_manual_reloading assert cleaned end + def test_prepend_prepare_callback + i = 10 + Reloader.to_prepare { i += 1 } + Reloader.to_prepare(:prepend => true) { i = 0 } + + Reloader.prepare! + assert_equal 1, i + end + def test_cleanup_callbacks_are_called_on_exceptions cleaned = false Reloader.to_cleanup { cleaned = true } diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index c2e31579a44ad..71772529d20d2 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -94,6 +94,11 @@ class Railtie < Rails::Railtie end end + initializer "active_record.add_watchable_files" do |app| + files = ["#{app.root}/db/schema.rb", "#{app.root}/db/structure.sql"] + config.watchable_files.concat files.select { |f| File.exist?(f) } + end + config.after_initialize do ActiveSupport.on_load(:active_record) do instantiate_observers diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb index 77bc5388d6551..4137bbf6a0b2b 100644 --- a/activesupport/lib/active_support/file_update_checker.rb +++ b/activesupport/lib/active_support/file_update_checker.rb @@ -39,30 +39,53 @@ def initialize(paths, calculate=false, &block) @paths = paths @glob = compile_glob(@paths.extract_options!) @block = block + @updated_at = nil @last_update_at = calculate ? updated_at : nil end - def updated_at - all = [] - all.concat @paths - all.concat Dir[@glob] if @glob - all.map { |path| File.mtime(path) }.max + # Check if any of the entries were updated. If so, the updated_at + # value is cached until flush! is called. + def updated? + current_updated_at = updated_at + if @last_update_at != current_updated_at + @updated_at = updated_at + true + else + false + end end + # Flush the cache so updated? is calculated again + def flush! + @updated_at = nil + end + + # Execute the block given if updated. This call + # always flush the cache. def execute_if_updated - current_update_at = self.updated_at - if @last_update_at != current_update_at - @last_update_at = current_update_at + if updated? + @last_update_at = updated_at @block.call true else false end + ensure + flush! end private - def compile_glob(hash) + def updated_at #:nodoc: + @updated_at || begin + all = [] + all.concat @paths + all.concat Dir[@glob] if @glob + all.map { |path| File.mtime(path) }.max + end + end + + def compile_glob(hash) #:nodoc: return if hash.empty? globs = [] hash.each do |key, value| @@ -71,7 +94,7 @@ def compile_glob(hash) "{#{globs.join(",")}}" end - def compile_ext(array) + def compile_ext(array) #:nodoc: array = Array.wrap(array) return if array.empty? ".{#{array.join(",")}}" diff --git a/activesupport/lib/active_support/i18n_railtie.rb b/activesupport/lib/active_support/i18n_railtie.rb index 4c59fe9ac9ca7..a989ff8f57cfd 100644 --- a/activesupport/lib/active_support/i18n_railtie.rb +++ b/activesupport/lib/active_support/i18n_railtie.rb @@ -17,7 +17,8 @@ def self.reloader # point, no path was added to the reloader, I18n.reload! is not triggered # on to_prepare callbacks. This will only happen on the config.after_initialize # callback below. - initializer "i18n.callbacks" do + initializer "i18n.callbacks" do |app| + app.reloaders << I18n::Railtie.reloader ActionDispatch::Reloader.to_prepare do I18n::Railtie.reloader.execute_if_updated end diff --git a/activesupport/test/file_update_checker_test.rb b/activesupport/test/file_update_checker_test.rb index a5a9b7a6821db..52c1f3260d2e2 100644 --- a/activesupport/test/file_update_checker_test.rb +++ b/activesupport/test/file_update_checker_test.rb @@ -54,6 +54,19 @@ def test_should_invoke_the_block_if_a_file_has_changed assert_equal 1, i end + def test_should_cache_updated_result_until_flushed + i = 0 + checker = ActiveSupport::FileUpdateChecker.new(FILES, true){ i += 1 } + assert !checker.updated? + + sleep(1) + FileUtils.touch(FILES) + + assert checker.updated? + assert checker.execute_if_updated + assert !checker.updated? + end + def test_should_invoke_the_block_if_a_watched_dir_changed_its_glob i = 0 checker = ActiveSupport::FileUpdateChecker.new([{"tmp_watcher" => [:txt]}], true){ i += 1 } diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 2841996b567b7..a88f443517eb0 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,9 +1,8 @@ ## Rails 3.2.0 (unreleased) ## -* New applications get a flag - `config.active_record.auto_explain_threshold_in_seconds` in the environments - configuration files. With a value of 0.5 in development.rb, and commented - out in production.rb. No mention in test.rb. *fxn* +* Speed up development by only reloading classes if dependencies files changed. This can be turned off by setting `config.reload_classes_only_on_change` to false. *José Valim* + +* New applications get a flag `config.active_record.auto_explain_threshold_in_seconds` in the environments configuration files. With a value of 0.5 in development.rb, and commented out in production.rb. No mention in test.rb. *fxn* * Add DebugExceptions middleware which contains features extracted from ShowExceptions middleware *José Valim* diff --git a/railties/guides/source/configuring.textile b/railties/guides/source/configuring.textile index 8e65dbccbb9c7..8cf88cf71f294 100644 --- a/railties/guides/source/configuring.textile +++ b/railties/guides/source/configuring.textile @@ -98,6 +98,8 @@ NOTE. The +config.asset_path+ configuration is ignored if the asset pipeline is * +config.preload_frameworks+ enables or disables preloading all frameworks at startup. Enabled by +config.threadsafe!+. Defaults to +nil+, so is disabled. +* +config.reload_classes_only_on_change+ enables or disables reloading of classes only when tracked files change. By default tracks everything on autoload paths and is set to true. + * +config.reload_plugins+ enables or disables plugin reloading. Defaults to false. * +config.secret_token+ used for specifying a key which allows sessions for the application to be verified against a known secure key to prevent tampering. Applications get +config.secret_token+ initialized to a random key in +config/initializers/secret_token.rb+. diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index acbfd7078ba10..0b8eac8a8b06d 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -71,12 +71,14 @@ def inherited(base) attr_accessor :assets, :sandbox alias_method :sandbox?, :sandbox + attr_reader :reloaders delegate :default_url_options, :default_url_options=, :to => :routes def initialize super @initialized = false + @reloaders = [] end # This method is called just after an application inherits from Rails::Application, @@ -119,6 +121,7 @@ def set_routes_reloader_hook reloader = routes_reloader hook = lambda { reloader.execute_if_updated } hook.call + self.reloaders << reloader ActionDispatch::Reloader.to_prepare(&hook) end @@ -126,10 +129,35 @@ def set_routes_reloader_hook # A plugin may override this if they desire to provide a more exquisite app reloading. # :api: plugin def set_dependencies_hook - ActionDispatch::Reloader.to_cleanup do + callback = lambda do ActiveSupport::DescendantsTracker.clear ActiveSupport::Dependencies.clear end + + if config.reload_classes_only_on_change + reloader = ActiveSupport::FileUpdateChecker.new(watchable_args, true, &callback) + self.reloaders << reloader + # We need to set a to_prepare callback regardless of the reloader result, i.e. + # models should be reloaded if any of the reloaders (i18n, routes) were updated. + ActionDispatch::Reloader.to_prepare(:prepend => true, &callback) + else + ActionDispatch::Reloader.to_cleanup(&callback) + end + end + + # Returns an array of file paths appended with a hash of directories-extensions + # suitable for ActiveSupport::FileUpdateChecker API. + def watchable_args + files = [] + files.concat config.watchable_files + + dirs = {} + dirs.merge! config.watchable_dirs + ActiveSupport::Dependencies.autoload_paths.each do |path| + dirs[path.to_s] = [:rb] + end + + files << dirs end # Initialize the application passing the given group. By default, the @@ -223,6 +251,10 @@ def helpers_paths #:nodoc: alias :build_middleware_stack :app + def reload_dependencies? + config.reload_classes_only_on_change != true || reloaders.map(&:updated?).any? + end + def default_middleware_stack ActionDispatch::MiddlewareStack.new.tap do |middleware| if rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache @@ -252,7 +284,11 @@ def default_middleware_stack middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header end - middleware.use ::ActionDispatch::Reloader unless config.cache_classes + unless config.cache_classes + app = self + middleware.use ::ActionDispatch::Reloader, lambda { app.reload_dependencies? } + end + middleware.use ::ActionDispatch::Callbacks middleware.use ::ActionDispatch::Cookies diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 4b2afe3a28df6..39d66ecc31260 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -7,11 +7,11 @@ class Application class Configuration < ::Rails::Engine::Configuration attr_accessor :allow_concurrency, :asset_host, :asset_path, :assets, :cache_classes, :cache_store, :consider_all_requests_local, - :dependency_loading, :filter_parameters, - :force_ssl, :helpers_paths, :logger, :log_tags, :preload_frameworks, - :relative_url_root, :reload_plugins, :secret_token, :serve_static_assets, - :ssl_options, :static_cache_control, :session_options, - :time_zone, :whiny_nils, :railties_order, :all_initializers + :dependency_loading, :filter_parameters, :force_ssl, :helpers_paths, + :initializers_paths, :logger, :log_tags, :preload_frameworks, + :railties_order, :relative_url_root, :reload_plugins, :secret_token, + :serve_static_assets, :ssl_options, :static_cache_control, :session_options, + :time_zone, :reload_classes_only_on_change, :whiny_nils attr_writer :log_level attr_reader :encoding @@ -19,25 +19,26 @@ class Configuration < ::Rails::Engine::Configuration def initialize(*) super self.encoding = "utf-8" - @allow_concurrency = false - @consider_all_requests_local = false - @filter_parameters = [] - @helpers_paths = [] - @dependency_loading = true - @serve_static_assets = true - @static_cache_control = nil - @force_ssl = false - @ssl_options = {} - @session_store = :cookie_store - @session_options = {} - @time_zone = "UTC" - @log_level = nil - @middleware = app_middleware - @generators = app_generators - @cache_store = [ :file_store, "#{root}/tmp/cache/" ] - @railties_order = [:all] - @all_initializers = [] - @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] + @allow_concurrency = false + @consider_all_requests_local = false + @filter_parameters = [] + @helpers_paths = [] + @dependency_loading = true + @serve_static_assets = true + @static_cache_control = nil + @force_ssl = false + @ssl_options = {} + @session_store = :cookie_store + @session_options = {} + @time_zone = "UTC" + @log_level = nil + @middleware = app_middleware + @generators = app_generators + @cache_store = [ :file_store, "#{root}/tmp/cache/" ] + @railties_order = [:all] + @initializers_paths = [] + @relative_url_root = ENV["RAILS_RELATIVE_URL_ROOT"] + @reload_classes_only_on_change = true @assets = ActiveSupport::OrderedOptions.new @assets.enabled = false diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 17e7aa0f28fc8..e000f6ef3ac2f 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -5,7 +5,7 @@ module Finisher $rails_rake_task = nil initializer :load_config_initializers do - config.all_initializers.each { |init| load(init) } + config.initializers_paths.each { |init| load(init) } end initializer :add_generator_templates do @@ -64,18 +64,18 @@ module Finisher ActiveSupport.run_load_hooks(:after_initialize, self) end - # Set app reload just after the finisher hook to ensure - # paths added in the hook are still loaded. - initializer :set_dependencies_hook, :group => :all do |app| - app.set_dependencies_hook - end - # Set app reload just after the finisher hook to ensure # routes added in the hook are still loaded. initializer :set_routes_reloader_hook do |app| app.set_routes_reloader_hook end + # Set app reload just after the finisher hook to ensure + # paths added in the hook are still loaded. + initializer :set_dependencies_hook, :group => :all do |app| + app.set_dependencies_hook + end + # Disable dependency loading during request cycle initializer :disable_dependency_loading do if config.cache_classes && !config.dependency_loading diff --git a/railties/lib/rails/application/routes_reloader.rb b/railties/lib/rails/application/routes_reloader.rb index c1f435a3eef49..460b84dfd9565 100644 --- a/railties/lib/rails/application/routes_reloader.rb +++ b/railties/lib/rails/application/routes_reloader.rb @@ -1,21 +1,17 @@ +require "active_support/core_ext/module/delegation" + module Rails class Application class RoutesReloader attr_reader :route_sets + delegate :paths, :execute_if_updated, :updated?, :to => :@updater + def initialize(updater=ActiveSupport::FileUpdateChecker) @updater = updater.new([]) { reload! } @route_sets = [] end - def paths - @updater.paths - end - - def execute_if_updated - @updater.execute_if_updated - end - def reload! clear! load_paths diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 8ebe1f48a5f56..86efc7332cb29 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -584,7 +584,7 @@ def load_seed end initializer :append_config_initializers do |app| - app.config.all_initializers.concat config.paths["config/initializers"].existent.sort + app.config.initializers_paths.concat config.paths["config/initializers"].existent.sort end initializer :engines_blank_point do diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb index f888684117e5e..cf9e4ad5005ae 100644 --- a/railties/lib/rails/railtie/configuration.rb +++ b/railties/lib/rails/railtie/configuration.rb @@ -7,6 +7,18 @@ def initialize @@options ||= {} end + # Add files that should be watched for change. + def watchable_files + @@watchable_files ||= [] + end + + # Add directories that should be watched for change. + # The key of the hashes should be directories and the values should + # be an array of extensions to match in each directory. + def watchable_dirs + @@watchable_dirs ||= {} + end + # This allows you to modify the application's middlewares from Engines. # # All operations you run on the app_middleware will be replayed on the diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb index 2073c780bfee5..6f9d8d57b1963 100644 --- a/railties/test/application/console_test.rb +++ b/railties/test/application/console_test.rb @@ -61,7 +61,8 @@ class User load_environment assert User.new.respond_to?(:name) - assert !User.new.respond_to?(:age) + + sleep(1) app_file "app/models/user.rb", <<-MODEL class User diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 47c6fd5c6e378..c4908915dc11d 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -66,6 +66,7 @@ class User < ActiveRecord::Base def test_descendants_are_cleaned_on_each_request_without_cache_classes add_to_config <<-RUBY config.cache_classes = false + config.reload_classes_only_on_change = false RUBY app_file "app/models/post.rb", <<-MODEL