From 0ce664a505a0362c782483ff7de21a5e4f92f5ea Mon Sep 17 00:00:00 2001 From: thedarkone Date: Tue, 10 Feb 2009 01:20:25 +0100 Subject: [PATCH] Add reloadable templates. --- actionpack/lib/action_controller/dispatcher.rb | 7 +- actionpack/lib/action_view.rb | 1 + actionpack/lib/action_view/base.rb | 9 ++ actionpack/lib/action_view/paths.rb | 6 +- actionpack/lib/action_view/reloadable_template.rb | 124 +++++++++++++++++++++ actionpack/lib/action_view/template.rb | 55 ++------- railties/environments/production.rb | 1 + railties/environments/test.rb | 1 + railties/lib/initializer.rb | 18 +--- 9 files changed, 161 insertions(+), 61 deletions(-) create mode 100644 actionpack/lib/action_view/reloadable_template.rb diff --git a/actionpack/lib/action_controller/dispatcher.rb b/actionpack/lib/action_controller/dispatcher.rb index e91babd..91bb061 100644 --- a/actionpack/lib/action_controller/dispatcher.rb +++ b/actionpack/lib/action_controller/dispatcher.rb @@ -3,7 +3,7 @@ module ActionController # reloading the app after each request when Dependencies.load? is true. class Dispatcher class << self - def define_dispatcher_callbacks(cache_classes) + def define_dispatcher_callbacks(cache_classes, cache_templates = true) unless cache_classes # Development mode callbacks before_dispatch :reload_application @@ -11,6 +11,11 @@ module ActionController ActionView::Helpers::AssetTagHelper.cache_asset_timestamps = false end + unless cache_templates + ActionView::ReloadableTemplate.register_new_request_callback!(ActionMailer::Base) if Rails.configuration.frameworks.include?(:action_mailer) + ActionView::ReloadableTemplate.register_new_request_callback!(ActionController::Base) + end + if defined?(ActiveRecord) after_dispatch :checkin_connections to_prepare(:activerecord_instantiate_observers) { ActiveRecord::Base.instantiate_observers } diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb index 0b710bd..1f1ff9d 100644 --- a/actionpack/lib/action_view.rb +++ b/actionpack/lib/action_view.rb @@ -44,6 +44,7 @@ module ActionView autoload :Renderable, 'action_view/renderable' autoload :RenderablePartial, 'action_view/renderable_partial' autoload :Template, 'action_view/template' + autoload :ReloadableTemplate, 'action_view/reloadable_template' autoload :TemplateError, 'action_view/template_error' autoload :TemplateHandler, 'action_view/template_handler' autoload :TemplateHandlers, 'action_view/template_handlers' diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 70a0ba9..f83be72 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -182,6 +182,15 @@ module ActionView #:nodoc: # that alert()s the caught exception (and then re-raises it). cattr_accessor :debug_rjs + # Specify whether templates should be cached. Otherwise the file we be read everytime it is accessed. + # Automaticaly reloading templates are not thread safe and should only be used in development mode. + @@cache_template_loading = false + cattr_accessor :cache_template_loading + + def self.cache_template_loading? + ActionController::Base.allow_concurrency || cache_template_loading + end + attr_internal :request delegate :request_forgery_protection_token, :template, :params, :session, :cookies, :response, :headers, diff --git a/actionpack/lib/action_view/paths.rb b/actionpack/lib/action_view/paths.rb index b487bd1..d15709b 100644 --- a/actionpack/lib/action_view/paths.rb +++ b/actionpack/lib/action_view/paths.rb @@ -2,10 +2,10 @@ module ActionView #:nodoc: class PathSet < Array #:nodoc: def self.type_cast(obj) if obj.is_a?(String) - if !Object.const_defined?(:Rails) || Rails.configuration.cache_classes + if Base.cache_template_loading? Template::EagerPath.new(obj) else - Template::Path.new(obj) + ReloadableTemplate::ReloadablePath.new(obj) end else obj @@ -57,7 +57,7 @@ module ActionView #:nodoc: end end - Template.new(original_template_path, self) + File.file?(template_path) ? Template.new(template_path, template_path =~ /\A\// ? "" : ".") : raise(MissingTemplate.new(self, template_path, format)) end end end diff --git a/actionpack/lib/action_view/reloadable_template.rb b/actionpack/lib/action_view/reloadable_template.rb new file mode 100644 index 0000000..8b70727 --- /dev/null +++ b/actionpack/lib/action_view/reloadable_template.rb @@ -0,0 +1,124 @@ +module ActionView #:nodoc: + class ReloadableTemplate < Template + + class TemplateDeleted < ActionView::ActionViewError + end + + class ReloadablePath < Template::Path + + def initialize(path) + super + @paths = {} + new_request! + end + + def new_request! + @disk_cache = {} + end + + def [](path) + if found_template = @paths[path] + begin + found_template.reset_cache_if_stale! + rescue TemplateDeleted + unregister_template(found_template) + self[path] + end + else + load_all_templates_from_dir(templates_dir_from_path(path)) + @paths[path] + end + end + + def register_template_from_file(template_file_path) + if !@paths[template_relative_path = template_file_path.split("#{@path}/").last] && File.file?(template_file_path) + register_template(ReloadableTemplate.new(template_relative_path, self)) + end + end + + def register_template(template) + template.accessible_paths.each do |path| + @paths[path] = template + end + end + + # remove (probably deleted) template from cache + def unregister_template(template) + template.accessible_paths.each do |template_path| + @paths.delete(template_path) if @paths[template_path] == template + end + # fill in any newly created gaps + @paths.values.uniq.each do |template| + template.accessible_paths.each {|path| @paths[path] ||= template} + end + end + + # load all templates from the directory of the requested template + def load_all_templates_from_dir(dir) + # hit disk only once per template-dir/request + @disk_cache[dir] ||= template_files_from_dir(dir).each {|template_file| register_template_from_file(template_file)} + end + + def templates_dir_from_path(path) + File.join(@path, File.dirname(path)) + end + + # get all the template filenames from the dir + def template_files_from_dir(dir) + Dir.glob("#{dir}/*") + end + + end + + module Unfreezable + def freeze; self; end + end + + def initialize(*args) + super + @compiled_methods = [] + + # we don't ever want to get frozen + extend Unfreezable + end + + def mtime + File.mtime(filename) + end + + attr_reader :previously_last_modified + + def stale? + previously_last_modified.nil? || previously_last_modified < mtime + rescue Errno::ENOENT => e + undef_my_compiled_methods! + raise TemplateDeleted + end + + def reset_cache_if_stale! + if stale? + flush_cache 'source', 'compiled_source' + undef_my_compiled_methods! + @previously_last_modified = mtime + end + self + end + + def undef_my_compiled_methods! + @compiled_methods.each {|comp_method| ActionView::Base::CompiledTemplates.send :remove_method, comp_method} + @compiled_methods.clear + end + + def compile!(render_symbol, local_assigns) + super + @compiled_methods << render_symbol + end + + def self.register_new_request_callback!(base) + ActionController::Dispatcher.to_prepare do + base.view_paths.each{|view_path| view_path.new_request! if view_path.respond_to?(:new_request!)} + end + end + + end +end diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb index 575ec7c..090a2cd 100644 --- a/actionpack/lib/action_view/template.rb +++ b/actionpack/lib/action_view/template.rb @@ -39,24 +39,7 @@ module ActionView #:nodoc: # etc. A format must be supplied to match a formated file. +hello/index+ # will never match +hello/index.html.erb+. def [](path) - templates_in_path do |template| - if template.accessible_paths.include?(path) - return template - end - end - nil end - - private - def templates_in_path - (Dir.glob("#{@path}/**/*/**") | Dir.glob("#{@path}/**")).each do |file| - yield create_template(file) unless File.directory?(file) - end - end - - def create_template(file) - Template.new(file.split("#{self}/").last, self) - end end class EagerPath < Path @@ -76,6 +59,17 @@ module ActionView #:nodoc: def [](path) @paths[path] end + + private + def templates_in_path + (Dir.glob("#{@path}/**/*/**") | Dir.glob("#{@path}/**")).each do |file| + yield create_template(file) unless File.directory?(file) + end + end + + def create_template(file) + Template.new(file.split("#{self}/").last, self) + end end extend TemplateHandlers @@ -97,9 +91,9 @@ module ActionView #:nodoc: attr_accessor :locale, :name, :format, :extension delegate :to_s, :to => :path - def initialize(template_path, load_paths = []) + def initialize(template_path, load_path) template_path = template_path.dup - @load_path, @filename = find_full_path(template_path, load_paths) + @load_path, @filename = load_path, File.join(load_path, template_path) @base_path, @name, @locale, @format, @extension = split(template_path) @base_path.to_s.gsub!(/\/$/, '') # Push to split method @@ -163,11 +157,6 @@ module ActionView #:nodoc: @@exempt_from_layout.any? { |exempted| path =~ exempted } end - def mtime - File.mtime(filename) - end - memoize :mtime - def source File.read(filename) end @@ -190,16 +179,7 @@ module ActionView #:nodoc: end end - def stale? - File.mtime(filename) > mtime - end - - def recompile? - !@cached - end - def load! - @cached = true freeze end @@ -212,15 +192,6 @@ module ActionView #:nodoc: I18n.available_locales.include?(locale.to_sym) end - def find_full_path(path, load_paths) - load_paths = Array(load_paths) + [nil] - load_paths.each do |load_path| - file = load_path ? "#{load_path.to_str}/#{path}" : path - return load_path, file if File.file?(file) - end - raise MissingTemplate.new(load_paths, path) - end - # Returns file split into an array # [base_path, name, locale, format, extension] def split(file) diff --git a/railties/environments/production.rb b/railties/environments/production.rb index 1fc9f6b..27119d2 100644 --- a/railties/environments/production.rb +++ b/railties/environments/production.rb @@ -7,6 +7,7 @@ config.cache_classes = true # Full error reports are disabled and caching is turned on config.action_controller.consider_all_requests_local = false config.action_controller.perform_caching = true +config.action_view.cache_template_loading = true # See everything in the log (default is :info) # config.log_level = :debug diff --git a/railties/environments/test.rb b/railties/environments/test.rb index 496eb95..d6f80a4 100644 --- a/railties/environments/test.rb +++ b/railties/environments/test.rb @@ -12,6 +12,7 @@ config.whiny_nils = true # Show full error reports and disable caching config.action_controller.consider_all_requests_local = true config.action_controller.perform_caching = false +config.action_view.cache_template_loading = true # Disable request forgery protection in test environment config.action_controller.allow_forgery_protection = false diff --git a/railties/lib/initializer.rb b/railties/lib/initializer.rb index e3811dd..52e072d 100644 --- a/railties/lib/initializer.rb +++ b/railties/lib/initializer.rb @@ -181,9 +181,6 @@ module Rails # Observers are loaded after plugins in case Observers or observed models are modified by plugins. load_observers - # Load view path cache - load_view_paths - # Load application classes load_application_classes @@ -367,16 +364,6 @@ Run `rake gems:install` to install the missing gems. end end - def load_view_paths - if configuration.frameworks.include?(:action_view) - if configuration.cache_classes - view_path = ActionView::Template::EagerPath.new(configuration.view_path) - ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) - ActionMailer::Base.template_root = view_path if configuration.frameworks.include?(:action_mailer) - end - end - end - # Eager load application classes def load_application_classes return if $rails_rake_task @@ -481,7 +468,7 @@ Run `rake gems:install` to install the missing gems. # set to use Configuration#view_path. def initialize_framework_views if configuration.frameworks.include?(:action_view) - view_path = ActionView::Template::Path.new(configuration.view_path) + view_path = ActionView::PathSet.type_cast(configuration.view_path) ActionMailer::Base.template_root ||= view_path if configuration.frameworks.include?(:action_mailer) ActionController::Base.view_paths = view_path if configuration.frameworks.include?(:action_controller) && ActionController::Base.view_paths.empty? end @@ -585,7 +572,8 @@ Run `rake gems:install` to install the missing gems. def prepare_dispatcher return unless configuration.frameworks.include?(:action_controller) require 'dispatcher' unless defined?(::Dispatcher) - Dispatcher.define_dispatcher_callbacks(configuration.cache_classes) + cache_templates = configuration.frameworks.include?(:action_view) && ActionView::Base.cache_template_loading? + Dispatcher.define_dispatcher_callbacks(configuration.cache_classes, cache_templates) Dispatcher.new(Rails.logger).send :run_callbacks, :prepare_dispatch end -- 1.6.0.1