Skip to content

Commit 45448a5

Browse files
committed
Replace some global Hash usages with the new thread safe cache.
Summary of the changes: * Add thread_safe gem. * Use thread safe cache for digestor caching. * Replace manual synchronization with ThreadSafe::Cache in Relation::Delegation. * Replace @attribute_method_matchers_cache Hash with ThreadSafe::Cache. * Use TS::Cache to avoid the synchronisation overhead on listener retrieval. * Replace synchronisation with TS::Cache usage. * Use a preallocated array for performance/memory reasons. * Update the controllers cache to the new AS::Dependencies::ClassCache API. The original @controllers cache no longer makes much sense after @tenderlove's changes in 7b6bfe8 and f345e23. * Use TS::Cache in the connection pool to avoid locking overhead. * Use TS::Cache in ConnectionHandler.
1 parent d668544 commit 45448a5

File tree

15 files changed

+101
-108
lines changed

15 files changed

+101
-108
lines changed

Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ gem 'jquery-rails', '~> 2.1.4', github: 'rails/jquery-rails'
1212
gem 'turbolinks'
1313
gem 'coffee-rails', github: 'rails/coffee-rails'
1414

15+
gem 'thread_safe', '~> 0.1'
1516
gem 'journey', github: 'rails/journey', branch: 'master'
1617

1718
gem 'activerecord-deprecated_finders', github: 'rails/activerecord-deprecated_finders', branch: 'master'

actionpack/lib/action_dispatch/http/filter_parameters.rb

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
require 'mutex_m'
1+
require 'thread_safe'
22
require 'active_support/core_ext/hash/keys'
33
require 'active_support/core_ext/object/duplicable'
44

@@ -21,7 +21,7 @@ module Http
2121
# end
2222
# => reverses the value to all keys matching /secret/i
2323
module FilterParameters
24-
@@parameter_filter_for = {}.extend(Mutex_m)
24+
@@parameter_filter_for = ThreadSafe::Cache.new
2525

2626
ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
2727
NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
@@ -65,11 +65,7 @@ def env_filter
6565
end
6666

6767
def parameter_filter_for(filters)
68-
@@parameter_filter_for.synchronize do
69-
# Do we *actually* need this cache? Constructing ParameterFilters
70-
# doesn't seem too expensive.
71-
@@parameter_filter_for[filters] ||= ParameterFilter.new(filters)
72-
end
68+
@@parameter_filter_for[filters] ||= ParameterFilter.new(filters)
7369
end
7470

7571
KV_RE = '[^&;=]+'

actionpack/lib/action_dispatch/routing/route_set.rb

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'journey'
22
require 'forwardable'
3+
require 'thread_safe'
34
require 'active_support/core_ext/object/to_query'
45
require 'active_support/core_ext/hash/slice'
56
require 'active_support/core_ext/module/remove_method'
@@ -20,7 +21,7 @@ class Dispatcher #:nodoc:
2021
def initialize(options={})
2122
@defaults = options[:defaults]
2223
@glob_param = options.delete(:glob)
23-
@controllers = {}
24+
@controller_class_names = ThreadSafe::Cache.new
2425
end
2526

2627
def call(env)
@@ -68,13 +69,8 @@ def controller(params, default_controller=true)
6869
private
6970

7071
def controller_reference(controller_param)
71-
controller_name = "#{controller_param.camelize}Controller"
72-
73-
unless controller = @controllers[controller_param]
74-
controller = @controllers[controller_param] =
75-
ActiveSupport::Dependencies.reference(controller_name)
76-
end
77-
controller.get(controller_name)
72+
const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"
73+
ActiveSupport::Dependencies.constantize(const_name)
7874
end
7975

8076
def dispatch(controller, action, env)

actionpack/lib/action_view/digestor.rb

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
require 'mutex_m'
1+
require 'thread_safe'
22

33
module ActionView
44
class Digestor
@@ -21,23 +21,12 @@ class Digestor
2121
/x
2222

2323
cattr_reader(:cache)
24-
@@cache = Hash.new.extend Mutex_m
24+
@@cache = ThreadSafe::Cache.new
2525

2626
def self.digest(name, format, finder, options = {})
27-
cache.synchronize do
28-
unsafe_digest name, format, finder, options
29-
end
30-
end
31-
32-
###
33-
# This method is NOT thread safe. DO NOT CALL IT DIRECTLY, instead call
34-
# Digestor.digest
35-
def self.unsafe_digest(name, format, finder, options = {}) # :nodoc:
36-
key = "#{name}.#{format}"
37-
38-
cache.fetch(key) do
27+
@@cache["#{name}.#{format}"] ||= begin
3928
klass = options[:partial] || name.include?("/_") ? PartialDigestor : Digestor
40-
cache[key] = klass.new(name, format, finder).digest
29+
klass.new(name, format, finder).digest
4130
end
4231
end
4332

@@ -93,7 +82,7 @@ def source
9382

9483
def dependency_digest
9584
dependencies.collect do |template_name|
96-
Digestor.unsafe_digest(template_name, format, finder, partial: true)
85+
Digestor.digest(template_name, format, finder, partial: true)
9786
end.join("-")
9887
end
9988

actionpack/lib/action_view/lookup_context.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require 'thread_safe'
12
require 'active_support/core_ext/module/remove_method'
23

34
module ActionView
@@ -51,7 +52,7 @@ class DetailsKey #:nodoc:
5152
alias :object_hash :hash
5253

5354
attr_reader :hash
54-
@details_keys = Hash.new
55+
@details_keys = ThreadSafe::Cache.new
5556

5657
def self.get(details)
5758
@details_keys[details] ||= new

actionpack/lib/action_view/renderer/partial_renderer.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
require 'thread_safe'
2+
13
module ActionView
24
# = Action View Partials
35
#
@@ -247,7 +249,9 @@ module ActionView
247249
# <%- end -%>
248250
# <% end %>
249251
class PartialRenderer < AbstractRenderer
250-
PREFIXED_PARTIAL_NAMES = Hash.new { |h,k| h[k] = {} }
252+
PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k|
253+
h[k] = ThreadSafe::Cache.new
254+
end
251255

252256
def initialize(*)
253257
super

actionpack/lib/action_view/template/resolver.rb

Lines changed: 29 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
require "active_support/core_ext/class/attribute_accessors"
44
require "action_view/template"
55
require "thread"
6-
require "mutex_m"
6+
require "thread_safe"
77

88
module ActionView
99
# = Action View Resolver
@@ -35,52 +35,51 @@ def to_str
3535

3636
# Threadsafe template cache
3737
class Cache #:nodoc:
38-
class CacheEntry
39-
include Mutex_m
40-
41-
attr_accessor :templates
38+
class SmallCache < ThreadSafe::Cache
39+
def initialize(options = {})
40+
super(options.merge(:initial_capacity => 2))
41+
end
4242
end
4343

44+
# preallocate all the default blocks for performance/memory consumption reasons
45+
PARTIAL_BLOCK = lambda {|cache, partial| cache[partial] = SmallCache.new}
46+
PREFIX_BLOCK = lambda {|cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK)}
47+
NAME_BLOCK = lambda {|cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK)}
48+
KEY_BLOCK = lambda {|cache, key| cache[key] = SmallCache.new(&NAME_BLOCK)}
49+
50+
# usually a majority of template look ups return nothing, use this canonical preallocated array to safe memory
51+
NO_TEMPLATES = [].freeze
52+
4453
def initialize
45-
@data = Hash.new { |h1,k1| h1[k1] = Hash.new { |h2,k2|
46-
h2[k2] = Hash.new { |h3,k3| h3[k3] = Hash.new { |h4,k4| h4[k4] = {} } } } }
47-
@mutex = Mutex.new
54+
@data = SmallCache.new(&KEY_BLOCK)
4855
end
4956

5057
# Cache the templates returned by the block
5158
def cache(key, name, prefix, partial, locals)
52-
cache_entry = nil
53-
54-
# first obtain a lock on the main data structure to create the cache entry
55-
@mutex.synchronize do
56-
cache_entry = @data[key][name][prefix][partial][locals] ||= CacheEntry.new
57-
end
58-
59-
# then to avoid a long lasting global lock, obtain a more granular lock
60-
# on the CacheEntry itself
61-
cache_entry.synchronize do
62-
if Resolver.caching?
63-
cache_entry.templates ||= yield
59+
if Resolver.caching?
60+
@data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
61+
else
62+
fresh_templates = yield
63+
cached_templates = @data[key][name][prefix][partial][locals]
64+
65+
if templates_have_changed?(cached_templates, fresh_templates)
66+
@data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates)
6467
else
65-
fresh_templates = yield
66-
67-
if templates_have_changed?(cache_entry.templates, fresh_templates)
68-
cache_entry.templates = fresh_templates
69-
else
70-
cache_entry.templates ||= []
71-
end
68+
cached_templates || NO_TEMPLATES
7269
end
7370
end
7471
end
7572

7673
def clear
77-
@mutex.synchronize do
78-
@data.clear
79-
end
74+
@data.clear
8075
end
8176

8277
private
8378

79+
def canonical_no_templates(templates)
80+
templates.empty? ? NO_TEMPLATES : templates
81+
end
82+
8483
def templates_have_changed?(cached_templates, fresh_templates)
8584
# if either the old or new template list is empty, we don't need to (and can't)
8685
# compare modification times, and instead just check whether the lists are different

activemodel/lib/active_model/attribute_methods.rb

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
require 'thread_safe'
12

23
module ActiveModel
34
# Raised when an attribute is not defined.
@@ -337,17 +338,17 @@ def instance_method_already_implemented?(method_name) #:nodoc:
337338
# significantly (in our case our test suite finishes 10% faster with
338339
# this cache).
339340
def attribute_method_matchers_cache #:nodoc:
340-
@attribute_method_matchers_cache ||= {}
341+
@attribute_method_matchers_cache ||= ThreadSafe::Cache.new(:initial_capacity => 4)
341342
end
342343

343344
def attribute_method_matcher(method_name) #:nodoc:
344-
attribute_method_matchers_cache.fetch(method_name) do |name|
345+
attribute_method_matchers_cache.compute_if_absent(method_name) do
345346
# Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
346347
# will match every time.
347348
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
348349
match = nil
349-
matchers.detect { |method| match = method.match(name) }
350-
attribute_method_matchers_cache[name] = match
350+
matchers.detect { |method| match = method.match(method_name) }
351+
match
351352
end
352353
end
353354

activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'thread'
2+
require 'thread_safe'
23
require 'monitor'
34
require 'set'
45
require 'active_support/deprecation'
@@ -236,9 +237,6 @@ def initialize(spec)
236237

237238
@spec = spec
238239

239-
# The cache of reserved connections mapped to threads
240-
@reserved_connections = {}
241-
242240
@checkout_timeout = spec.config[:checkout_timeout] || 5
243241
@dead_connection_timeout = spec.config[:dead_connection_timeout]
244242
@reaper = Reaper.new self, spec.config[:reaping_frequency]
@@ -247,6 +245,9 @@ def initialize(spec)
247245
# default max pool size to 5
248246
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
249247

248+
# The cache of reserved connections mapped to threads
249+
@reserved_connections = ThreadSafe::Cache.new(:initial_capacity => @size)
250+
250251
@connections = []
251252
@automatic_reconnect = true
252253

@@ -267,7 +268,9 @@ def insert_connection_for_test!(c) #:nodoc:
267268
# #connection can be called any number of times; the connection is
268269
# held in a hash keyed by the thread id.
269270
def connection
270-
synchronize do
271+
# this is correctly done double-checked locking
272+
# (ThreadSafe::Cache's lookups have volatile semantics)
273+
@reserved_connections[current_connection_id] || synchronize do
271274
@reserved_connections[current_connection_id] ||= checkout
272275
end
273276
end
@@ -310,7 +313,7 @@ def connected?
310313
# Disconnects all connections in the pool, and clears the pool.
311314
def disconnect!
312315
synchronize do
313-
@reserved_connections = {}
316+
@reserved_connections.clear
314317
@connections.each do |conn|
315318
checkin conn
316319
conn.disconnect!
@@ -323,7 +326,7 @@ def disconnect!
323326
# Clears the cache which maps classes.
324327
def clear_reloadable_connections!
325328
synchronize do
326-
@reserved_connections = {}
329+
@reserved_connections.clear
327330
@connections.each do |conn|
328331
checkin conn
329332
conn.disconnect! if conn.requires_reloading?
@@ -490,11 +493,15 @@ def checkout_and_verify(c)
490493
# determine the connection pool that they should use.
491494
class ConnectionHandler
492495
def initialize
493-
# These hashes are keyed by klass.name, NOT klass. Keying them by klass
496+
# These caches are keyed by klass.name, NOT klass. Keying them by klass
494497
# alone would lead to memory leaks in development mode as all previous
495498
# instances of the class would stay in memory.
496-
@owner_to_pool = Hash.new { |h,k| h[k] = {} }
497-
@class_to_pool = Hash.new { |h,k| h[k] = {} }
499+
@owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
500+
h[k] = ThreadSafe::Cache.new(:initial_capacity => 2)
501+
end
502+
@class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
503+
h[k] = ThreadSafe::Cache.new
504+
end
498505
end
499506

500507
def connection_pool_list

activerecord/lib/active_record/relation/delegation.rb

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'active_support/concern'
2-
require 'mutex_m'
2+
require 'thread'
3+
require 'thread_safe'
34

45
module ActiveRecord
56
module Delegation # :nodoc:
@@ -73,42 +74,35 @@ def method_missing(method, *args, &block)
7374
end
7475

7576
module ClassMethods
76-
# This hash is keyed by klass.name to avoid memory leaks in development mode
77-
@@subclasses = Hash.new { |h, k| h[k] = {} }.extend(Mutex_m)
77+
@@subclasses = ThreadSafe::Cache.new(:initial_capacity => 2)
7878

7979
def new(klass, *args)
8080
relation = relation_class_for(klass).allocate
8181
relation.__send__(:initialize, klass, *args)
8282
relation
8383
end
8484

85+
# This doesn't have to be thread-safe. relation_class_for guarantees that this will only be
86+
# called exactly once for a given const name.
87+
def const_missing(name)
88+
const_set(name, Class.new(self) { include ClassSpecificRelation })
89+
end
90+
91+
private
8592
# Cache the constants in @@subclasses because looking them up via const_get
8693
# make instantiation significantly slower.
8794
def relation_class_for(klass)
88-
if klass && klass.name
89-
if subclass = @@subclasses.synchronize { @@subclasses[self][klass.name] }
90-
subclass
91-
else
92-
subclass = const_get("#{name.gsub('::', '_')}_#{klass.name.gsub('::', '_')}", false)
93-
@@subclasses.synchronize { @@subclasses[self][klass.name] = subclass }
94-
subclass
95+
if klass && (klass_name = klass.name)
96+
my_cache = @@subclasses.compute_if_absent(self) { ThreadSafe::Cache.new }
97+
# This hash is keyed by klass.name to avoid memory leaks in development mode
98+
my_cache.compute_if_absent(klass_name) do
99+
# Cache#compute_if_absent guarantees that the block will only executed once for the given klass_name
100+
const_get("#{name.gsub('::', '_')}_#{klass_name.gsub('::', '_')}", false)
95101
end
96102
else
97103
ActiveRecord::Relation
98104
end
99105
end
100-
101-
# Check const_defined? in case another thread has already defined the constant.
102-
# I am not sure whether this is strictly necessary.
103-
def const_missing(name)
104-
@@subclasses.synchronize {
105-
if const_defined?(name)
106-
const_get(name)
107-
else
108-
const_set(name, Class.new(self) { include ClassSpecificRelation })
109-
end
110-
}
111-
end
112106
end
113107

114108
def respond_to?(method, include_private = false)

0 commit comments

Comments
 (0)