identity_cache 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +5 -4
  3. data/.github/workflows/cla.yml +22 -0
  4. data/.rubocop.yml +7 -3
  5. data/.spin/bootstrap +7 -0
  6. data/.spin/svc.yml +2 -0
  7. data/CHANGELOG.md +21 -0
  8. data/Gemfile +15 -5
  9. data/LICENSE +1 -1
  10. data/README.md +5 -6
  11. data/Rakefile +13 -12
  12. data/dev.yml +2 -4
  13. data/gemfiles/Gemfile.latest-release +12 -7
  14. data/gemfiles/Gemfile.min-supported +11 -6
  15. data/gemfiles/Gemfile.rails-edge +8 -5
  16. data/identity_cache.gemspec +15 -27
  17. data/{railgun.yml → isogun.yml} +0 -5
  18. data/lib/identity_cache/belongs_to_caching.rb +1 -0
  19. data/lib/identity_cache/cache_fetcher.rb +241 -16
  20. data/lib/identity_cache/cache_hash.rb +7 -6
  21. data/lib/identity_cache/cache_invalidation.rb +1 -0
  22. data/lib/identity_cache/cache_key_generation.rb +22 -19
  23. data/lib/identity_cache/cache_key_loader.rb +3 -3
  24. data/lib/identity_cache/cached/association.rb +2 -4
  25. data/lib/identity_cache/cached/attribute.rb +9 -5
  26. data/lib/identity_cache/cached/attribute_by_multi.rb +1 -1
  27. data/lib/identity_cache/cached/belongs_to.rb +3 -0
  28. data/lib/identity_cache/cached/embedded_fetching.rb +2 -0
  29. data/lib/identity_cache/cached/primary_index.rb +3 -2
  30. data/lib/identity_cache/cached/recursive/association.rb +2 -0
  31. data/lib/identity_cache/cached/recursive/has_many.rb +1 -0
  32. data/lib/identity_cache/cached/recursive/has_one.rb +1 -0
  33. data/lib/identity_cache/cached/reference/association.rb +1 -0
  34. data/lib/identity_cache/cached/reference/has_many.rb +1 -0
  35. data/lib/identity_cache/cached/reference/has_one.rb +1 -0
  36. data/lib/identity_cache/cached.rb +1 -0
  37. data/lib/identity_cache/configuration_dsl.rb +1 -0
  38. data/lib/identity_cache/encoder.rb +2 -1
  39. data/lib/identity_cache/expiry_hook.rb +1 -0
  40. data/lib/identity_cache/fallback_fetcher.rb +6 -1
  41. data/lib/identity_cache/mem_cache_store_cas.rb +15 -5
  42. data/lib/identity_cache/memoized_cache_proxy.rb +15 -15
  43. data/lib/identity_cache/parent_model_expiration.rb +6 -3
  44. data/lib/identity_cache/query_api.rb +13 -6
  45. data/lib/identity_cache/railtie.rb +1 -0
  46. data/lib/identity_cache/should_use_cache.rb +1 -0
  47. data/lib/identity_cache/version.rb +2 -1
  48. data/lib/identity_cache/with_primary_index.rb +49 -12
  49. data/lib/identity_cache/without_primary_index.rb +7 -3
  50. data/lib/identity_cache.rb +38 -24
  51. data/performance/cache_runner.rb +12 -9
  52. data/performance/cpu.rb +6 -5
  53. data/performance/externals.rb +6 -5
  54. data/performance/profile.rb +7 -6
  55. metadata +27 -123
  56. data/.github/probots.yml +0 -2
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require 'dalli/cas/client'
2
+
3
+ require "dalli/cas/client" unless Dalli::VERSION > "3"
3
4
 
4
5
  module IdentityCache
5
6
  module MemCacheStoreCAS
@@ -11,7 +12,7 @@ module IdentityCache
11
12
  instrument(:cas, key, options) do
12
13
  @data.with do |connection|
13
14
  connection.cas(key, options[:expires_in].to_i, options) do |raw_value|
14
- entry = deserialize_entry(raw_value)
15
+ entry = deserialize_entry(raw_value, raw: options[:raw])
15
16
  value = yield entry.value
16
17
  entry = ActiveSupport::Cache::Entry.new(value, **options)
17
18
  options[:raw] ? entry.value.to_s : entry
@@ -29,11 +30,11 @@ module IdentityCache
29
30
  keys = keys_to_names.keys
30
31
  rescue_error_with(false) do
31
32
  instrument(:cas_multi, keys, options) do
32
- raw_values = @data.get_multi_cas(keys)
33
+ raw_values = @data.with { |c| c.get_multi_cas(keys) }
33
34
 
34
35
  values = {}
35
36
  raw_values.each do |key, raw_value|
36
- entry = deserialize_entry(raw_value.first)
37
+ entry = deserialize_entry(raw_value.first, raw: options[:raw])
37
38
  values[keys_to_names[key]] = entry.value unless entry.expired?
38
39
  end
39
40
 
@@ -44,10 +45,19 @@ module IdentityCache
44
45
  cas_id = raw_values[key].last
45
46
  entry = ActiveSupport::Cache::Entry.new(value, **options)
46
47
  payload = options[:raw] ? entry.value.to_s : entry
47
- @data.replace_cas(key, payload, cas_id, options[:expires_in].to_i, options)
48
+ @data.with { |c| c.replace_cas(key, payload, cas_id, options[:expires_in].to_i, options) }
48
49
  end
49
50
  end
50
51
  end
51
52
  end
53
+
54
+ if ActiveSupport::Cache::MemCacheStore.instance_method(:deserialize_entry).arity == 1
55
+
56
+ private
57
+
58
+ def deserialize_entry(payload, raw: nil)
59
+ super(payload)
60
+ end
61
+ end
52
62
  end
53
63
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
- require 'monitor'
3
- require 'benchmark'
2
+
3
+ require "monitor"
4
+ require "benchmark"
4
5
 
5
6
  module IdentityCache
6
7
  class MemoizedCacheProxy
@@ -12,7 +13,7 @@ module IdentityCache
12
13
  end
13
14
 
14
15
  def cache_backend=(cache_adaptor)
15
- if cache_adaptor.class.name == 'ActiveSupport::Cache::MemCacheStore'
16
+ if cache_adaptor.class.name == "ActiveSupport::Cache::MemCacheStore"
16
17
  if cache_adaptor.respond_to?(:cas) || cache_adaptor.respond_to?(:cas_multi)
17
18
  unless cache_adaptor.is_a?(MemCacheStoreCAS)
18
19
  raise "#{cache_adaptor} respond to :cas or :cas_multi, that's unexpected"
@@ -30,7 +31,7 @@ module IdentityCache
30
31
  # no need for CAS support
31
32
  else
32
33
  warn("[IdentityCache] Missing CAS support in cache backend #{cache_adaptor.class} "\
33
- "which is needed for cache consistency")
34
+ "which is needed for cache consistency")
34
35
  end
35
36
  @cache_fetcher = FallbackFetcher.new(cache_adaptor)
36
37
  end
@@ -50,7 +51,7 @@ module IdentityCache
50
51
 
51
52
  def write(key, value)
52
53
  memoizing = memoizing?
53
- ActiveSupport::Notifications.instrument('cache_write.identity_cache', memoizing: memoizing) do
54
+ ActiveSupport::Notifications.instrument("cache_write.identity_cache", memoizing: memoizing) do
54
55
  memoized_key_values[key] = value if memoizing
55
56
  @cache_fetcher.write(key, value)
56
57
  end
@@ -58,7 +59,7 @@ module IdentityCache
58
59
 
59
60
  def delete(key)
60
61
  memoizing = memoizing?
61
- ActiveSupport::Notifications.instrument('cache_delete.identity_cache', memoizing: memoizing) do
62
+ ActiveSupport::Notifications.instrument("cache_delete.identity_cache", memoizing: memoizing) do
62
63
  memoized_key_values.delete(key) if memoizing
63
64
  if (result = @cache_fetcher.delete(key))
64
65
  IdentityCache.logger.debug { "[IdentityCache] delete recorded for #{key}" }
@@ -69,20 +70,18 @@ module IdentityCache
69
70
  end
70
71
  end
71
72
 
72
- def fetch(key)
73
+ def fetch(key, cache_fetcher_options = {}, &block)
73
74
  memo_misses = 0
74
75
  cache_misses = 0
75
76
 
76
- value = ActiveSupport::Notifications.instrument('cache_fetch.identity_cache') do |payload|
77
+ value = ActiveSupport::Notifications.instrument("cache_fetch.identity_cache") do |payload|
77
78
  payload[:resolve_miss_time] = 0.0
78
79
 
79
80
  value = fetch_memoized(key) do
80
81
  memo_misses = 1
81
- @cache_fetcher.fetch(key) do
82
+ @cache_fetcher.fetch(key, **cache_fetcher_options) do
82
83
  cache_misses = 1
83
- instrument_duration(payload, :resolve_miss_time) do
84
- yield
85
- end
84
+ instrument_duration(payload, :resolve_miss_time, &block)
86
85
  end
87
86
  end
88
87
  set_instrumentation_payload(payload, num_keys: 1, memo_misses: memo_misses, cache_misses: cache_misses)
@@ -93,7 +92,7 @@ module IdentityCache
93
92
  IdentityCache.logger.debug { "[IdentityCache] cache miss for #{key}" }
94
93
  else
95
94
  IdentityCache.logger.debug do
96
- "[IdentityCache] #{memo_misses > 0 ? '(cache_backend)' : '(memoized)'} cache hit for #{key}"
95
+ "[IdentityCache] #{memo_misses > 0 ? "(cache_backend)" : "(memoized)"} cache hit for #{key}"
97
96
  end
98
97
  end
99
98
 
@@ -104,7 +103,7 @@ module IdentityCache
104
103
  memo_miss_keys = EMPTY_ARRAY
105
104
  cache_miss_keys = EMPTY_ARRAY
106
105
 
107
- result = ActiveSupport::Notifications.instrument('cache_fetch_multi.identity_cache') do |payload|
106
+ result = ActiveSupport::Notifications.instrument("cache_fetch_multi.identity_cache") do |payload|
108
107
  payload[:resolve_miss_time] = 0.0
109
108
 
110
109
  result = fetch_multi_memoized(keys) do |non_memoized_keys|
@@ -132,7 +131,7 @@ module IdentityCache
132
131
  end
133
132
 
134
133
  def clear
135
- ActiveSupport::Notifications.instrument('cache_clear.identity_cache') do
134
+ ActiveSupport::Notifications.instrument("cache_clear.identity_cache") do
136
135
  clear_memoization
137
136
  @cache_fetcher.clear
138
137
  end
@@ -155,6 +154,7 @@ module IdentityCache
155
154
  if memoized_key_values.key?(key)
156
155
  return memoized_key_values[key]
157
156
  end
157
+
158
158
  memoized_key_values[key] = yield
159
159
  end
160
160
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module ParentModelExpiration # :nodoc:
4
5
  extend ActiveSupport::Concern
@@ -22,6 +23,7 @@ module IdentityCache
22
23
 
23
24
  def install_pending_parent_expiry_hooks(model)
24
25
  return if lazy_hooks.empty?
26
+
25
27
  name = model.name.demodulize
26
28
  if (hooks = lazy_hooks.delete(name))
27
29
  hooks.each(&:install)
@@ -43,8 +45,9 @@ module IdentityCache
43
45
  def expire_parent_caches
44
46
  parents_to_expire = Set.new
45
47
  add_parents_to_cache_expiry_set(parents_to_expire)
46
- parents_to_expire.each do |parent|
47
- parent.expire_primary_index if parent.class.primary_cache_index_enabled
48
+ parents_to_expire.select! { |parent| parent.class.primary_cache_index_enabled }
49
+ parents_to_expire.reduce(true) do |all_expired, parent|
50
+ parent.expire_primary_index && all_expired
48
51
  end
49
52
  end
50
53
 
@@ -85,7 +88,7 @@ module IdentityCache
85
88
 
86
89
  cached_associations.each do |parent_class, only_on_foreign_key_change|
87
90
  if new_parent&.is_a?(parent_class) &&
88
- should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
91
+ should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
89
92
  add_record_to_cache_expiry_set(parents_to_expire, new_parent)
90
93
  end
91
94
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module QueryAPI
4
5
  extend ActiveSupport::Concern
@@ -68,6 +69,7 @@ module IdentityCache
68
69
  def setup_embedded_associations_on_miss(records,
69
70
  readonly: IdentityCache.fetch_read_only_records && should_use_cache?)
70
71
  return if records.empty?
72
+
71
73
  records.each(&:readonly!) if readonly
72
74
  each_id_embedded_association do |cached_association|
73
75
  preload_id_embedded_association(records, cached_association)
@@ -86,6 +88,7 @@ module IdentityCache
86
88
  association.reset
87
89
  # reset inverse associations
88
90
  next unless target && association_reflection.has_inverse?
91
+
89
92
  inverse_name = association_reflection.inverse_of.name
90
93
  if target.is_a?(Array)
91
94
  target.each { |child_record| child_record.association(inverse_name).reset }
@@ -163,15 +166,17 @@ module IdentityCache
163
166
  def _run_commit_callbacks
164
167
  if destroyed? || transaction_changed_attributes.present?
165
168
  expire_cache
166
- expire_parent_caches
167
169
  end
168
170
  super
169
171
  end
170
172
 
171
- # Invalidate the cache data associated with the record.
173
+ # Invalidate the cache data associated with the record. Returns `true` on success,
174
+ # `false` otherwise.
172
175
  def expire_cache
173
- expire_attribute_indexes
174
- true
176
+ expired_parent_caches = expire_parent_caches
177
+ expired_attribute_indexes = expire_attribute_indexes
178
+
179
+ expired_parent_caches && expired_attribute_indexes
175
180
  end
176
181
 
177
182
  # @api private
@@ -182,9 +187,11 @@ module IdentityCache
182
187
 
183
188
  private
184
189
 
190
+ # Even if we have problems with some attributes, carry over the results and expire
191
+ # all possible attributes without array allocation.
185
192
  def expire_attribute_indexes # :nodoc:
186
- cache_indexes.each do |cached_attribute|
187
- cached_attribute.expire(self)
193
+ cache_indexes.reduce(true) do |all_expired, cached_attribute|
194
+ cached_attribute.expire(self) && all_expired
188
195
  end
189
196
  end
190
197
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  class Railtie < Rails::Railtie
4
5
  initializer "identity_cache.setup" do |app|
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module ShouldUseCache
4
5
  extend ActiveSupport::Concern
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
- VERSION = "1.1.0"
4
+ VERSION = "1.3.0"
4
5
  CACHE_VERSION = 8
5
6
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module WithPrimaryIndex
4
5
  extend ActiveSupport::Concern
@@ -6,8 +7,9 @@ module IdentityCache
6
7
  include WithoutPrimaryIndex
7
8
 
8
9
  def expire_cache
9
- expire_primary_index
10
- super
10
+ expired_primary_index = expire_primary_index
11
+
12
+ super && expired_primary_index
11
13
  end
12
14
 
13
15
  # @api private
@@ -57,7 +59,7 @@ module IdentityCache
57
59
  cache_attribute_by_alias(attribute_proc, alias_name: :id, by: fields, unique: unique)
58
60
 
59
61
  field_list = fields.join("_and_")
60
- arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
62
+ arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(",")
61
63
 
62
64
  if unique
63
65
  instance_eval(<<-CODE, __FILE__, __LINE__ + 1)
@@ -97,21 +99,47 @@ module IdentityCache
97
99
  !!fetch_by_id(id)
98
100
  end
99
101
 
100
- # Default fetcher added to the model on inclusion, it behaves like
101
- # ActiveRecord::Base.where(id: id).first
102
- def fetch_by_id(id, includes: nil)
102
+ # Fetch the record by its primary key from the cache or read from
103
+ # the database and fill the cache on a cache miss. This behaves like
104
+ # `where(id: id).readonly.first` being called on the model.
105
+ #
106
+ # @param id Primary key value for the record to fetch.
107
+ # @param includes [Hash|Array|Symbol] Cached associations to prefetch from
108
+ # the cache on the returned record
109
+ # @param fill_lock_duration [Float] If provided, take a fill lock around cache fills
110
+ # and wait for this duration for cache to be filled when reading a lock provided
111
+ # by another client. Defaults to not setting the fill lock and trying to fill the
112
+ # cache from the database regardless of the presence of another client's fill lock.
113
+ # Set this to just above the typical amount of time it takes to do a cache fill.
114
+ # @param lock_wait_tries [Integer] Only applicable if fill_lock_duration is provided,
115
+ # in which case it specifies the number of times to do a lock wait. After the first
116
+ # lock wait it will try to take the lock, so will only do following lock waits due
117
+ # to another client taking the lock first. If another lock wait would be needed after
118
+ # reaching the limit, then a `LockWaitTimeout` exception is raised. Default is 2. Use
119
+ # this to control the maximum total lock wait duration
120
+ # (`lock_wait_tries * fill_lock_duration`).
121
+ # @raise [LockWaitTimeout] Timeout after waiting `lock_wait_tries * fill_lock_duration`
122
+ # seconds for `lock_wait_tries` other clients to fill the cache.
123
+ # @return [self|nil] An instance of this model for the record with the specified id or
124
+ # `nil` if no record with this `id` exists in the database
125
+ def fetch_by_id(id, includes: nil, **cache_fetcher_options)
103
126
  ensure_base_model
104
127
  raise_if_scoped
105
- record = cached_primary_index.fetch(id)
128
+ record = cached_primary_index.fetch(id, cache_fetcher_options)
106
129
  prefetch_associations(includes, [record]) if record && includes
107
130
  record
108
131
  end
109
132
 
110
- # Default fetcher added to the model on inclusion, it behaves like
111
- # ActiveRecord::Base.find, but will raise IdentityCache::RecordNotFound
112
- # if the id is not in the cache.
113
- def fetch(id, includes: nil)
114
- fetch_by_id(id, includes: includes) || raise(
133
+ # Fetch the record by its primary key from the cache or read from
134
+ # the database and fill the cache on a cache miss. This behaves like
135
+ # `readonly.find(id)` being called on the model.
136
+ #
137
+ # @param (see #fetch_by_id)
138
+ # @raise (see #fetch_by_id)
139
+ # @raise [ActiveRecord::RecordNotFound] if the record isn't found
140
+ # @return [self] An instance of this model for the record with the specified id
141
+ def fetch(id, **options)
142
+ fetch_by_id(id, **options) || raise(
115
143
  IdentityCache::RecordNotFound, "Couldn't find #{name} with ID=#{id}"
116
144
  )
117
145
  end
@@ -131,6 +159,15 @@ module IdentityCache
131
159
  def expire_primary_key_cache_index(id)
132
160
  cached_primary_index.expire(id)
133
161
  end
162
+
163
+ private
164
+
165
+ def inherited(subclass)
166
+ super
167
+ subclass.class_eval do
168
+ @cached_primary_index = nil
169
+ end
170
+ end
134
171
  end
135
172
  end
136
173
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module WithoutPrimaryIndex
4
5
  extend ActiveSupport::Concern
@@ -12,9 +13,12 @@ module IdentityCache
12
13
  include IdentityCache::ShouldUseCache
13
14
  include ParentModelExpiration
14
15
 
15
- def self.append_features(base) #:nodoc:
16
- raise AlreadyIncludedError if base.include?(WithoutPrimaryIndex)
17
- super
16
+ class << self
17
+ def append_features(base) # :nodoc:
18
+ raise AlreadyIncludedError if base.include?(WithoutPrimaryIndex)
19
+
20
+ super
21
+ end
18
22
  end
19
23
 
20
24
  included do
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
- require 'active_record'
3
- require 'active_support/core_ext/module/attribute_accessors'
4
- require 'ar_transaction_changes'
2
+
3
+ require "active_record"
4
+ require "active_support/core_ext/module/attribute_accessors"
5
+ require "ar_transaction_changes"
5
6
 
6
7
  require "identity_cache/version"
7
8
  require "identity_cache/record_not_found"
@@ -27,24 +28,24 @@ require "identity_cache/cached/reference/association"
27
28
  require "identity_cache/cached/reference/has_one"
28
29
  require "identity_cache/cached/reference/has_many"
29
30
  require "identity_cache/expiry_hook"
30
- require 'identity_cache/memoized_cache_proxy'
31
- require 'identity_cache/belongs_to_caching'
32
- require 'identity_cache/cache_key_generation'
33
- require 'identity_cache/configuration_dsl'
34
- require 'identity_cache/should_use_cache'
35
- require 'identity_cache/parent_model_expiration'
36
- require 'identity_cache/query_api'
31
+ require "identity_cache/memoized_cache_proxy"
32
+ require "identity_cache/belongs_to_caching"
33
+ require "identity_cache/cache_key_generation"
34
+ require "identity_cache/configuration_dsl"
35
+ require "identity_cache/should_use_cache"
36
+ require "identity_cache/parent_model_expiration"
37
+ require "identity_cache/query_api"
37
38
  require "identity_cache/cache_hash"
38
39
  require "identity_cache/cache_invalidation"
39
40
  require "identity_cache/cache_fetcher"
40
41
  require "identity_cache/fallback_fetcher"
41
- require 'identity_cache/without_primary_index'
42
- require 'identity_cache/with_primary_index'
42
+ require "identity_cache/without_primary_index"
43
+ require "identity_cache/with_primary_index"
43
44
 
44
45
  module IdentityCache
45
46
  extend ActiveSupport::Concern
46
47
 
47
- autoload :MemCacheStoreCAS, 'identity_cache/mem_cache_store_cas'
48
+ autoload :MemCacheStoreCAS, "identity_cache/mem_cache_store_cas"
48
49
 
49
50
  include WithPrimaryIndex
50
51
 
@@ -65,23 +66,26 @@ module IdentityCache
65
66
 
66
67
  class DerivedModelError < StandardError; end
67
68
 
69
+ class LockWaitTimeout < StandardError; end
70
+
68
71
  mattr_accessor :cache_namespace
69
72
  self.cache_namespace = "IDC:#{CACHE_VERSION}:"
70
73
 
71
74
  # Fetched records are not read-only and this could sometimes prevent IDC from
72
75
  # reflecting what's truly in the database when fetch_read_only_records is false.
73
76
  # When set to true, it will only return read-only records when cache is used.
74
- mattr_accessor :fetch_read_only_records
75
- self.fetch_read_only_records = true
77
+ @fetch_read_only_records = true
76
78
 
77
79
  class << self
78
80
  include IdentityCache::CacheHash
79
81
 
82
+ attr_writer :fetch_read_only_records
80
83
  attr_accessor :readonly
81
84
  attr_writer :logger
82
85
 
83
- def append_features(base) #:nodoc:
86
+ def append_features(base) # :nodoc:
84
87
  raise AlreadyIncludedError if base.include?(IdentityCache)
88
+
85
89
  super
86
90
  end
87
91
 
@@ -141,10 +145,13 @@ module IdentityCache
141
145
  #
142
146
  # == Parameters
143
147
  # +key+ A cache key string
148
+ # +cache_fetcher_options+ A hash of options to pass to the cache backend
144
149
  #
145
- def fetch(key)
150
+ def fetch(key, cache_fetcher_options = {})
146
151
  if should_use_cache?
147
- unmap_cached_nil_for(cache.fetch(key) { map_cached_nil_for(yield) })
152
+ unmap_cached_nil_for(cache.fetch(key, cache_fetcher_options) do
153
+ map_cached_nil_for(yield)
154
+ end)
148
155
  else
149
156
  yield
150
157
  end
@@ -185,11 +192,18 @@ module IdentityCache
185
192
  end
186
193
 
187
194
  def with_fetch_read_only_records(value = true)
188
- old_value = fetch_read_only_records
189
- self.fetch_read_only_records = value
195
+ old_value = Thread.current[:identity_cache_fetch_read_only_records]
196
+ Thread.current[:identity_cache_fetch_read_only_records] = value
190
197
  yield
191
198
  ensure
192
- self.fetch_read_only_records = old_value
199
+ Thread.current[:identity_cache_fetch_read_only_records] = old_value
200
+ end
201
+
202
+ def fetch_read_only_records
203
+ v = Thread.current[:identity_cache_fetch_read_only_records]
204
+ return v unless v.nil?
205
+
206
+ @fetch_read_only_records
193
207
  end
194
208
 
195
209
  def eager_load!
@@ -198,12 +212,12 @@ module IdentityCache
198
212
 
199
213
  private
200
214
 
201
- def fetch_in_batches(keys)
215
+ def fetch_in_batches(keys, &block)
202
216
  keys.each_slice(BATCH_SIZE).each_with_object({}) do |slice, result|
203
- result.merge!(cache.fetch_multi(*slice) { |missed_keys| yield missed_keys })
217
+ result.merge!(cache.fetch_multi(*slice, &block))
204
218
  end
205
219
  end
206
220
  end
207
221
  end
208
222
 
209
- require 'identity_cache/railtie' if defined?(Rails)
223
+ require "identity_cache/railtie" if defined?(Rails)
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  $LOAD_PATH.unshift(File.expand_path("../../lib", __FILE__))
3
- require 'active_record'
4
- require 'active_support/core_ext'
5
- require 'active_support/cache'
6
- require 'identity_cache'
7
- require 'memcached_store'
8
- require 'active_support/cache/memcached_store'
4
+ require "active_record"
5
+ require "active_support/core_ext"
6
+ require "active_support/cache"
7
+ require "identity_cache"
8
+ require "memcached_store"
9
+ require "active_support/cache/memcached_store"
9
10
 
10
- require File.dirname(__FILE__) + '/../test/helpers/active_record_objects'
11
- require File.dirname(__FILE__) + '/../test/helpers/database_connection'
12
- require File.dirname(__FILE__) + '/../test/helpers/cache_connection'
11
+ require File.dirname(__FILE__) + "/../test/helpers/active_record_objects"
12
+ require File.dirname(__FILE__) + "/../test/helpers/database_connection"
13
+ require File.dirname(__FILE__) + "/../test/helpers/cache_connection"
13
14
 
14
15
  IdentityCache.logger = Logger.new(nil)
15
16
  CacheConnection.setup
@@ -31,6 +32,7 @@ def create_database(count)
31
32
  helper.setup_models
32
33
 
33
34
  return if database_ready(count)
35
+
34
36
  puts "Database not ready for performance testing, generating records"
35
37
 
36
38
  DatabaseConnection.drop_tables
@@ -38,6 +40,7 @@ def create_database(count)
38
40
  existing = Item.all
39
41
  (1..count).to_a.each do |i|
40
42
  next if existing.any? { |e| e.id == i }
43
+
41
44
  a = Item.new
42
45
  a.id = i
43
46
  a.associated = AssociatedRecord.new(name: "Associated for #{i}")
data/performance/cpu.rb CHANGED
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
- require 'rubygems'
3
- require 'benchmark'
4
2
 
5
- require_relative 'cache_runner'
3
+ require "rubygems"
4
+ require "benchmark"
5
+
6
+ require_relative "cache_runner"
6
7
 
7
8
  RUNS = 400
8
9
 
@@ -32,9 +33,9 @@ def bmbm(runners)
32
33
  label_width = runners.map { |r| r.name.size }.max + 2
33
34
  width = label_width + Benchmark::CAPTION.size
34
35
 
35
- puts 'Rehearsal: '.ljust(width, '-')
36
+ puts "Rehearsal: ".ljust(width, "-")
36
37
  benchmark(runners, label_width)
37
- puts '-' * width
38
+ puts "-" * width
38
39
 
39
40
  benchmark(runners, label_width)
40
41
  end
@@ -1,15 +1,16 @@
1
1
  # frozen_string_literal: true
2
- require 'rubygems'
3
- require 'benchmark'
4
- require 'ruby-prof'
5
2
 
6
- require_relative 'cache_runner'
3
+ require "rubygems"
4
+ require "benchmark"
5
+ require "ruby-prof"
6
+
7
+ require_relative "cache_runner"
7
8
 
8
9
  RUNS = 1000
9
10
  RubyProf.measure_mode = RubyProf::CPU_TIME
10
11
 
11
12
  EXTERNALS = { "Memcache" => ["MemCache#set", "MemCache#get"],
12
- "Database" => ["Mysql2::Client#query"] }
13
+ "Database" => ["Mysql2::Client#query"], }
13
14
 
14
15
  def run(obj)
15
16
  obj.prepare
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
- require 'rubygems'
3
- require 'benchmark'
4
- require 'stackprof'
5
2
 
6
- require_relative 'cache_runner'
3
+ require "rubygems"
4
+ require "benchmark"
5
+ require "stackprof"
6
+
7
+ require_relative "cache_runner"
7
8
 
8
9
  RUNS = 1000
9
10
 
@@ -22,9 +23,9 @@ end
22
23
 
23
24
  create_database(RUNS)
24
25
 
25
- if (runner_name = ENV['RUNNER'])
26
+ if (runner_name = ENV["RUNNER"])
26
27
  if (runner = CACHE_RUNNERS.find { |r| r.name == runner_name })
27
- run(runner.new(RUNS), filename: ENV['FILENAME'])
28
+ run(runner.new(RUNS), filename: ENV["FILENAME"])
28
29
  else
29
30
  puts "Couldn't find cache runner #{runner_name.inspect}"
30
31
  exit(1)