diff options
author | Aaron Patterson <aaron.patterson@gmail.com> | 2012-05-21 11:29:25 -0700 |
---|---|---|
committer | Aaron Patterson <aaron.patterson@gmail.com> | 2012-05-21 11:29:25 -0700 |
commit | 254c04286c5916ae7f91eb6e173b312e7a74e364 (patch) | |
tree | c1321e2d5d84977ae0a0b84a6a1ba8337d2512e3 | |
parent | 513a0525c24c2944630acfa465b22cd2f4601adf (diff) | |
parent | 719b008f1dae30c5fb6d09a371ae8d949c867a0c (diff) | |
download | rails-254c04286c5916ae7f91eb6e173b312e7a74e364.tar.gz rails-254c04286c5916ae7f91eb6e173b312e7a74e364.tar.bz2 rails-254c04286c5916ae7f91eb6e173b312e7a74e364.zip |
Merge pull request #6425 from pinetops/resolver_concurrency_fix
Resolver concurrency fix
-rw-r--r-- | actionpack/lib/action_view/template/resolver.rb | 87 | ||||
-rw-r--r-- | actionpack/test/template/lookup_context_test.rb | 8 |
2 files changed, 72 insertions, 23 deletions
diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb index fa2038f78d..8f87d6da8b 100644 --- a/actionpack/lib/action_view/template/resolver.rb +++ b/actionpack/lib/action_view/template/resolver.rb @@ -2,6 +2,7 @@ require "pathname" require "active_support/core_ext/class" require "active_support/core_ext/class/attribute_accessors" require "action_view/template" +require "thread" module ActionView # = Action View Resolver @@ -24,6 +25,64 @@ module ActionView end end + # Threadsafe template cache + class Cache #:nodoc: + class CacheEntry + attr_accessor :templates + + delegate :synchronize, :to => "@mutex" + + def initialize + @mutex = Mutex.new + end + end + + def initialize + @data = Hash.new { |h1,k1| h1[k1] = Hash.new { |h2,k2| + h2[k2] = Hash.new { |h3,k3| h3[k3] = Hash.new { |h4,k4| h4[k4] = {} } } } } + @mutex = Mutex.new + end + + # Cache the templates returned by the block + def cache(key, name, prefix, partial, locals) + cache_entry = nil + + # first obtain a lock on the main data structure to create the cache entry + @mutex.synchronize do + cache_entry = @data[key][name][prefix][partial][locals] ||= CacheEntry.new + end + + # then to avoid a long lasting global lock, obtain a more granular lock + # on the CacheEntry itself + cache_entry.synchronize do + if Resolver.caching? + # all templates are cached forever the first time they are accessed + cache_entry.templates ||= yield + else + # templates are still cached, but are only returned if they are + # all still current + fresh = yield + + mtime = cache_entry.templates && cache_entry.templates.map(&:updated_at).max + + newer = !mtime || fresh.empty? || fresh.any? { |t| t.updated_at > mtime } + + if newer + cache_entry.templates = fresh + else + cache_entry.templates + end + end + end + end + + def clear + @mutex.synchronize do + @data.clear + end + end + end + cattr_accessor :caching self.caching = true @@ -32,12 +91,11 @@ module ActionView end def initialize - @cached = Hash.new { |h1,k1| h1[k1] = Hash.new { |h2,k2| - h2[k2] = Hash.new { |h3,k3| h3[k3] = Hash.new { |h4,k4| h4[k4] = {} } } } } + @cache = Cache.new end def clear_cache - @cached.clear + @cache.clear end # Normalizes the arguments and passes it on to find_template. @@ -65,27 +123,18 @@ module ActionView # Handles templates caching. If a key is given and caching is on # always check the cache before hitting the resolver. Otherwise, - # it always hits the resolver but check if the resolver is fresher - # before returning it. + # it always hits the resolver but if the key is present, check if the + # resolver is fresher before returning it. def cached(key, path_info, details, locals) #:nodoc: name, prefix, partial = path_info locals = locals.map { |x| x.to_s }.sort! - if key && caching? - @cached[key][name][prefix][partial][locals] ||= decorate(yield, path_info, details, locals) - else - fresh = decorate(yield, path_info, details, locals) - return fresh unless key - - scope = @cached[key][name][prefix][partial] - cache = scope[locals] - mtime = cache && cache.map(&:updated_at).max - - if !mtime || fresh.empty? || fresh.any? { |t| t.updated_at > mtime } - scope[locals] = fresh - else - cache + if key + @cache.cache(key, name, prefix, partial, locals) do + decorate(yield, path_info, details, locals) end + else + decorate(yield, path_info, details, locals) end end diff --git a/actionpack/test/template/lookup_context_test.rb b/actionpack/test/template/lookup_context_test.rb index 96b14a0acd..ef9c5ce10c 100644 --- a/actionpack/test/template/lookup_context_test.rb +++ b/actionpack/test/template/lookup_context_test.rb @@ -169,7 +169,7 @@ class LookupContextTest < ActiveSupport::TestCase assert_not_equal template, old_template end - + test "responds to #prefixes" do assert_equal [], @lookup_context.prefixes @lookup_context.prefixes = ["foo"] @@ -180,7 +180,7 @@ end class LookupContextWithFalseCaching < ActiveSupport::TestCase def setup @resolver = ActionView::FixtureResolver.new("test/_foo.erb" => ["Foo", Time.utc(2000)]) - @resolver.stubs(:caching?).returns(false) + ActionView::Resolver.stubs(:caching?).returns(false) @lookup_context = ActionView::LookupContext.new(@resolver, {}) end @@ -247,6 +247,6 @@ class TestMissingTemplate < ActiveSupport::TestCase @lookup_context.view_paths.find("foo", "parent", true, details) end assert_match %r{Missing partial parent/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message - end - + end + end |