identity_cache 0.4.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (94) hide show
  1. checksums.yaml +5 -5
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/ci.yml +92 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +5 -0
  6. data/CAVEATS.md +25 -0
  7. data/CHANGELOG.md +73 -19
  8. data/Gemfile +5 -1
  9. data/LICENSE +1 -1
  10. data/README.md +49 -27
  11. data/Rakefile +14 -5
  12. data/dev.yml +12 -16
  13. data/gemfiles/Gemfile.latest-release +8 -0
  14. data/gemfiles/Gemfile.min-supported +7 -0
  15. data/gemfiles/Gemfile.rails-edge +7 -0
  16. data/identity_cache.gemspec +29 -10
  17. data/lib/identity_cache.rb +78 -51
  18. data/lib/identity_cache/belongs_to_caching.rb +12 -40
  19. data/lib/identity_cache/cache_fetcher.rb +6 -5
  20. data/lib/identity_cache/cache_hash.rb +2 -2
  21. data/lib/identity_cache/cache_invalidation.rb +4 -11
  22. data/lib/identity_cache/cache_key_generation.rb +17 -65
  23. data/lib/identity_cache/cache_key_loader.rb +128 -0
  24. data/lib/identity_cache/cached.rb +7 -0
  25. data/lib/identity_cache/cached/association.rb +87 -0
  26. data/lib/identity_cache/cached/attribute.rb +123 -0
  27. data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
  28. data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
  29. data/lib/identity_cache/cached/belongs_to.rb +100 -0
  30. data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
  31. data/lib/identity_cache/cached/prefetcher.rb +61 -0
  32. data/lib/identity_cache/cached/primary_index.rb +96 -0
  33. data/lib/identity_cache/cached/recursive/association.rb +109 -0
  34. data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
  35. data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
  36. data/lib/identity_cache/cached/reference/association.rb +16 -0
  37. data/lib/identity_cache/cached/reference/has_many.rb +105 -0
  38. data/lib/identity_cache/cached/reference/has_one.rb +100 -0
  39. data/lib/identity_cache/configuration_dsl.rb +53 -215
  40. data/lib/identity_cache/encoder.rb +95 -0
  41. data/lib/identity_cache/expiry_hook.rb +36 -0
  42. data/lib/identity_cache/fallback_fetcher.rb +2 -1
  43. data/lib/identity_cache/load_strategy/eager.rb +28 -0
  44. data/lib/identity_cache/load_strategy/lazy.rb +71 -0
  45. data/lib/identity_cache/load_strategy/load_request.rb +20 -0
  46. data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
  47. data/lib/identity_cache/mem_cache_store_cas.rb +53 -0
  48. data/lib/identity_cache/memoized_cache_proxy.rb +137 -58
  49. data/lib/identity_cache/parent_model_expiration.rb +46 -11
  50. data/lib/identity_cache/query_api.rb +102 -408
  51. data/lib/identity_cache/railtie.rb +8 -0
  52. data/lib/identity_cache/record_not_found.rb +6 -0
  53. data/lib/identity_cache/should_use_cache.rb +1 -0
  54. data/lib/identity_cache/version.rb +3 -2
  55. data/lib/identity_cache/with_primary_index.rb +136 -0
  56. data/lib/identity_cache/without_primary_index.rb +24 -3
  57. data/performance/cache_runner.rb +25 -73
  58. data/performance/cpu.rb +4 -3
  59. data/performance/externals.rb +4 -3
  60. data/performance/profile.rb +6 -5
  61. data/railgun.yml +16 -0
  62. metadata +60 -73
  63. data/.travis.yml +0 -30
  64. data/Gemfile.rails42 +0 -6
  65. data/Gemfile.rails50 +0 -6
  66. data/test/attribute_cache_test.rb +0 -110
  67. data/test/cache_fetch_includes_test.rb +0 -46
  68. data/test/cache_hash_test.rb +0 -14
  69. data/test/cache_invalidation_test.rb +0 -139
  70. data/test/deeply_nested_associated_record_test.rb +0 -19
  71. data/test/denormalized_has_many_test.rb +0 -211
  72. data/test/denormalized_has_one_test.rb +0 -160
  73. data/test/fetch_multi_test.rb +0 -308
  74. data/test/fetch_test.rb +0 -258
  75. data/test/fixtures/serialized_record.mysql2 +0 -0
  76. data/test/fixtures/serialized_record.postgresql +0 -0
  77. data/test/helpers/active_record_objects.rb +0 -106
  78. data/test/helpers/database_connection.rb +0 -72
  79. data/test/helpers/serialization_format.rb +0 -42
  80. data/test/helpers/update_serialization_format.rb +0 -24
  81. data/test/identity_cache_test.rb +0 -29
  82. data/test/index_cache_test.rb +0 -161
  83. data/test/memoized_attributes_test.rb +0 -49
  84. data/test/memoized_cache_proxy_test.rb +0 -107
  85. data/test/normalized_belongs_to_test.rb +0 -107
  86. data/test/normalized_has_many_test.rb +0 -231
  87. data/test/normalized_has_one_test.rb +0 -9
  88. data/test/prefetch_associations_test.rb +0 -364
  89. data/test/readonly_test.rb +0 -109
  90. data/test/recursive_denormalized_has_many_test.rb +0 -131
  91. data/test/save_test.rb +0 -82
  92. data/test/schema_change_test.rb +0 -112
  93. data/test/serialization_format_change_test.rb +0 -16
  94. data/test/test_helper.rb +0 -140
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ module IdentityCache
3
+ module Encoder
4
+ DEHYDRATE_EVENT = "dehydration.identity_cache"
5
+ HYDRATE_EVENT = "hydration.identity_cache"
6
+
7
+ class << self
8
+ def encode(record)
9
+ return unless record
10
+
11
+ ActiveSupport::Notifications.instrument(DEHYDRATE_EVENT, class: record.class.name) do
12
+ coder_from_record(record, record.class)
13
+ end
14
+ end
15
+
16
+ def decode(coder, klass)
17
+ return unless coder
18
+
19
+ ActiveSupport::Notifications.instrument(HYDRATE_EVENT, class: klass.name) do
20
+ record_from_coder(coder, klass)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def coder_from_record(record, klass)
27
+ return unless record
28
+
29
+ coder = {}
30
+ coder[:attributes] = record.attributes_before_type_cast.dup
31
+
32
+ recursively_embedded_associations = klass.send(:recursively_embedded_associations)
33
+ id_embedded_has_manys = klass.cached_has_manys.select { |_, association| association.embedded_by_reference? }
34
+ id_embedded_has_ones = klass.cached_has_ones.select { |_, association| association.embedded_by_reference? }
35
+
36
+ if recursively_embedded_associations.present?
37
+ coder[:associations] = recursively_embedded_associations.each_with_object({}) do |(name, association), hash|
38
+ hash[name] = IdentityCache.map_cached_nil_for(embedded_coder(record, name, association))
39
+ end
40
+ end
41
+
42
+ if id_embedded_has_manys.present?
43
+ coder[:association_ids] = id_embedded_has_manys.each_with_object({}) do |(name, association), hash|
44
+ hash[name] = record.instance_variable_get(association.ids_variable_name)
45
+ end
46
+ end
47
+
48
+ if id_embedded_has_ones.present?
49
+ coder[:association_id] = id_embedded_has_ones.each_with_object({}) do |(name, association), hash|
50
+ hash[name] = record.instance_variable_get(association.id_variable_name)
51
+ end
52
+ end
53
+
54
+ coder
55
+ end
56
+
57
+ def embedded_coder(record, _association, cached_association)
58
+ embedded_record_or_records = record.public_send(cached_association.cached_accessor_name)
59
+
60
+ if embedded_record_or_records.respond_to?(:to_ary)
61
+ embedded_record_or_records.map do |embedded_record|
62
+ coder_from_record(embedded_record, embedded_record.class)
63
+ end
64
+ else
65
+ coder_from_record(embedded_record_or_records, embedded_record_or_records.class)
66
+ end
67
+ end
68
+
69
+ def record_from_coder(coder, klass) #:nodoc:
70
+ record = klass.instantiate(coder[:attributes].dup)
71
+
72
+ if coder.key?(:associations)
73
+ coder[:associations].each do |name, value|
74
+ record.instance_variable_set(klass.cached_association(name).dehydrated_variable_name, value)
75
+ end
76
+ end
77
+ if coder.key?(:association_ids)
78
+ coder[:association_ids].each do |name, ids|
79
+ record.instance_variable_set(klass.cached_has_manys.fetch(name).ids_variable_name, ids)
80
+ end
81
+ end
82
+ if coder.key?(:association_id)
83
+ coder[:association_id].each do |name, id|
84
+ record.instance_variable_set(klass.cached_has_ones.fetch(name).id_variable_name, id)
85
+ end
86
+ end
87
+
88
+ record.readonly! if IdentityCache.fetch_read_only_records
89
+ record
90
+ end
91
+ end
92
+ end
93
+
94
+ private_constant :Encoder
95
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ module IdentityCache
3
+ class ExpiryHook
4
+ def initialize(cached_association)
5
+ @cached_association = cached_association
6
+ end
7
+
8
+ def install
9
+ cached_association.validate
10
+ entry = [parent_class, only_on_foreign_key_change?]
11
+ child_class.parent_expiration_entries[inverse_name] << entry
12
+ end
13
+
14
+ private
15
+
16
+ attr_reader :cached_association
17
+
18
+ def only_on_foreign_key_change?
19
+ cached_association.embedded_by_reference? && !cached_association.reflection.has_scope?
20
+ end
21
+
22
+ def inverse_name
23
+ cached_association.inverse_name
24
+ end
25
+
26
+ def parent_class
27
+ cached_association.reflection.active_record
28
+ end
29
+
30
+ def child_class
31
+ cached_association.reflection.klass
32
+ end
33
+ end
34
+
35
+ private_constant :ExpiryHook
36
+ end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module IdentityCache
2
3
  class FallbackFetcher
3
4
  attr_accessor :cache_backend
@@ -18,7 +19,7 @@ module IdentityCache
18
19
  @cache_backend.clear
19
20
  end
20
21
 
21
- def fetch_multi(keys, &block)
22
+ def fetch_multi(keys)
22
23
  results = @cache_backend.read_multi(*keys)
23
24
  missed_keys = keys - results.keys
24
25
  unless missed_keys.empty?
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IdentityCache
4
+ module LoadStrategy
5
+ module Eager
6
+ extend self
7
+
8
+ def load(cache_fetcher, db_key)
9
+ yield CacheKeyLoader.load(cache_fetcher, db_key)
10
+ end
11
+
12
+ def load_multi(cache_fetcher, db_keys)
13
+ yield CacheKeyLoader.load_multi(cache_fetcher, db_keys)
14
+ end
15
+
16
+ def load_batch(db_keys_by_cache_fetcher)
17
+ yield CacheKeyLoader.load_batch(db_keys_by_cache_fetcher)
18
+ end
19
+
20
+ def lazy_load
21
+ lazy_loader = Lazy.new
22
+ yield lazy_loader
23
+ lazy_loader.load_now
24
+ nil
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IdentityCache
4
+ module LoadStrategy
5
+ class Lazy
6
+ def initialize
7
+ @pending_loads = {}
8
+ end
9
+
10
+ def load(cache_fetcher, db_key)
11
+ load_multi(cache_fetcher, [db_key]) do |results|
12
+ yield results.fetch(db_key)
13
+ end
14
+ nil
15
+ end
16
+
17
+ def load_multi(cache_fetcher, db_keys, &callback)
18
+ load_request = LoadRequest.new(db_keys, callback)
19
+
20
+ if (prev_load_request = @pending_loads[cache_fetcher])
21
+ if prev_load_request.instance_of?(MultiLoadRequest)
22
+ prev_load_request.load_requests << load_request
23
+ else
24
+ @pending_loads[cache_fetcher] = MultiLoadRequest.new([prev_load_request, load_request])
25
+ end
26
+ else
27
+ @pending_loads[cache_fetcher] = LoadRequest.new(db_keys, callback)
28
+ end
29
+ nil
30
+ end
31
+
32
+ def load_batch(db_keys_by_cache_fetcher)
33
+ batch_result = {}
34
+ db_keys_by_cache_fetcher.each do |cache_fetcher, db_keys|
35
+ load_multi(cache_fetcher, db_keys) do |load_result|
36
+ batch_result[cache_fetcher] = load_result
37
+ if batch_result.size == db_keys_by_cache_fetcher.size
38
+ yield batch_result
39
+ end
40
+ end
41
+ end
42
+ nil
43
+ end
44
+
45
+ def lazy_load
46
+ yield self
47
+ nil
48
+ end
49
+
50
+ def load_now
51
+ until @pending_loads.empty?
52
+ pending_loads = @pending_loads
53
+ @pending_loads = {}
54
+ load_pending(pending_loads)
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def load_pending(pending_loads)
61
+ result = CacheKeyLoader.load_batch(pending_loads.transform_values(&:db_keys))
62
+ result.each do |cache_fetcher, load_result|
63
+ load_request = pending_loads.fetch(cache_fetcher)
64
+ load_request.after_load(load_result)
65
+ end
66
+ end
67
+ end
68
+
69
+ private_constant :Lazy
70
+ end
71
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IdentityCache
4
+ module LoadStrategy
5
+ class LoadRequest
6
+ attr_reader :db_keys
7
+
8
+ def initialize(db_keys, callback)
9
+ @db_keys = db_keys
10
+ @callback = callback
11
+ end
12
+
13
+ def after_load(results)
14
+ @callback.call(results)
15
+ end
16
+ end
17
+
18
+ private_constant :LoadRequest
19
+ end
20
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IdentityCache
4
+ module LoadStrategy
5
+ class MultiLoadRequest
6
+ def initialize(load_requests)
7
+ @load_requests = load_requests
8
+ end
9
+
10
+ def db_keys
11
+ @load_requests.flat_map(&:db_keys).tap(&:uniq!)
12
+ end
13
+
14
+ def after_load(all_results)
15
+ @load_requests.each do |load_request|
16
+ load_result = {}
17
+ load_request.db_keys.each do |key|
18
+ load_result[key] = all_results[key]
19
+ end
20
+ load_request.after_load(load_result)
21
+ end
22
+ end
23
+ end
24
+
25
+ private_constant :MultiLoadRequest
26
+ end
27
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ require 'dalli/cas/client'
3
+
4
+ module IdentityCache
5
+ module MemCacheStoreCAS
6
+ def cas(name, options = nil)
7
+ options = merged_options(options)
8
+ key = normalize_key(name, options)
9
+
10
+ rescue_error_with(false) do
11
+ instrument(:cas, key, options) do
12
+ @data.with do |connection|
13
+ connection.cas(key, options[:expires_in].to_i, options) do |raw_value|
14
+ entry = deserialize_entry(raw_value)
15
+ value = yield entry.value
16
+ entry = ActiveSupport::Cache::Entry.new(value, **options)
17
+ options[:raw] ? entry.value.to_s : entry
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+
24
+ def cas_multi(*names, **options)
25
+ return if names.empty?
26
+
27
+ options = merged_options(options)
28
+ keys_to_names = names.each_with_object({}) { |name, hash| hash[normalize_key(name, options)] = name }
29
+ keys = keys_to_names.keys
30
+ rescue_error_with(false) do
31
+ instrument(:cas_multi, keys, options) do
32
+ raw_values = @data.get_multi_cas(keys)
33
+
34
+ values = {}
35
+ raw_values.each do |key, raw_value|
36
+ entry = deserialize_entry(raw_value.first)
37
+ values[keys_to_names[key]] = entry.value unless entry.expired?
38
+ end
39
+
40
+ updates = yield values
41
+
42
+ updates.each do |name, value|
43
+ key = normalize_key(name, options)
44
+ cas_id = raw_values[key].last
45
+ entry = ActiveSupport::Cache::Entry.new(value, **options)
46
+ payload = options[:raw] ? entry.value.to_s : entry
47
+ @data.replace_cas(key, payload, cas_id, options[:expires_in].to_i, options)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,4 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  require 'monitor'
3
+ require 'benchmark'
2
4
 
3
5
  module IdentityCache
4
6
  class MemoizedCacheProxy
@@ -6,13 +8,30 @@ module IdentityCache
6
8
 
7
9
  def initialize(cache_adaptor = nil)
8
10
  self.cache_backend = cache_adaptor || Rails.cache
9
- @key_value_maps = Hash.new {|h, k| h[k] = {} }
11
+ @key_value_maps = Hash.new { |h, k| h[k] = {} }
10
12
  end
11
13
 
12
14
  def cache_backend=(cache_adaptor)
15
+ if cache_adaptor.class.name == 'ActiveSupport::Cache::MemCacheStore'
16
+ if cache_adaptor.respond_to?(:cas) || cache_adaptor.respond_to?(:cas_multi)
17
+ unless cache_adaptor.is_a?(MemCacheStoreCAS)
18
+ raise "#{cache_adaptor} respond to :cas or :cas_multi, that's unexpected"
19
+ end
20
+ else
21
+ cache_adaptor.extend(MemCacheStoreCAS)
22
+ end
23
+ end
24
+
13
25
  if cache_adaptor.respond_to?(:cas) && cache_adaptor.respond_to?(:cas_multi)
14
26
  @cache_fetcher = CacheFetcher.new(cache_adaptor)
15
27
  else
28
+ case cache_adaptor
29
+ when ActiveSupport::Cache::MemoryStore, ActiveSupport::Cache::NullStore
30
+ # no need for CAS support
31
+ else
32
+ warn("[IdentityCache] Missing CAS support in cache backend #{cache_adaptor.class} "\
33
+ "which is needed for cache consistency")
34
+ end
16
35
  @cache_fetcher = FallbackFetcher.new(cache_adaptor)
17
36
  end
18
37
  end
@@ -21,7 +40,7 @@ module IdentityCache
21
40
  @key_value_maps[Thread.current]
22
41
  end
23
42
 
24
- def with_memoization(&block)
43
+ def with_memoization
25
44
  Thread.current[:memoizing_idc] = true
26
45
  yield
27
46
  ensure
@@ -30,101 +49,161 @@ module IdentityCache
30
49
  end
31
50
 
32
51
  def write(key, value)
33
- memoized_key_values[key] = value if memoizing?
34
- @cache_fetcher.write(key, value)
52
+ memoizing = memoizing?
53
+ ActiveSupport::Notifications.instrument('cache_write.identity_cache', memoizing: memoizing) do
54
+ memoized_key_values[key] = value if memoizing
55
+ @cache_fetcher.write(key, value)
56
+ end
35
57
  end
36
58
 
37
59
  def delete(key)
38
- memoized_key_values.delete(key) if memoizing?
39
- result = @cache_fetcher.delete(key)
40
- IdentityCache.logger.debug { "[IdentityCache] delete #{ result ? 'recorded' : 'failed' } for #{key}" }
41
- result
60
+ memoizing = memoizing?
61
+ ActiveSupport::Notifications.instrument('cache_delete.identity_cache', memoizing: memoizing) do
62
+ memoized_key_values.delete(key) if memoizing
63
+ if (result = @cache_fetcher.delete(key))
64
+ IdentityCache.logger.debug { "[IdentityCache] delete recorded for #{key}" }
65
+ else
66
+ IdentityCache.logger.error { "[IdentityCache] delete failed for #{key}" }
67
+ end
68
+ result
69
+ end
42
70
  end
43
71
 
44
72
  def fetch(key)
45
- used_cache_backend = true
46
- missed = false
47
- value = if memoizing?
48
- used_cache_backend = false
49
- memoized_key_values.fetch(key) do
50
- used_cache_backend = true
51
- memoized_key_values[key] = @cache_fetcher.fetch(key) do
52
- missed = true
53
- yield
73
+ memo_misses = 0
74
+ cache_misses = 0
75
+
76
+ value = ActiveSupport::Notifications.instrument('cache_fetch.identity_cache') do |payload|
77
+ payload[:resolve_miss_time] = 0.0
78
+
79
+ value = fetch_memoized(key) do
80
+ memo_misses = 1
81
+ @cache_fetcher.fetch(key) do
82
+ cache_misses = 1
83
+ instrument_duration(payload, :resolve_miss_time) do
84
+ yield
85
+ end
54
86
  end
55
87
  end
56
- else
57
- @cache_fetcher.fetch(key) do
58
- missed = true
59
- yield
60
- end
88
+ set_instrumentation_payload(payload, num_keys: 1, memo_misses: memo_misses, cache_misses: cache_misses)
89
+ value
61
90
  end
62
91
 
63
- if missed
92
+ if cache_misses > 0
64
93
  IdentityCache.logger.debug { "[IdentityCache] cache miss for #{key}" }
65
94
  else
66
- IdentityCache.logger.debug { "[IdentityCache] #{ used_cache_backend ? '(cache_backend)' : '(memoized)' } cache hit for #{key}" }
95
+ IdentityCache.logger.debug do
96
+ "[IdentityCache] #{memo_misses > 0 ? '(cache_backend)' : '(memoized)'} cache hit for #{key}"
97
+ end
67
98
  end
68
99
 
69
100
  value
70
101
  end
71
102
 
72
103
  def fetch_multi(*keys)
73
- memoized_keys, missed_keys = [], [] if IdentityCache.logger.debug?
74
-
75
- result = if memoizing?
76
- hash = {}
77
- mkv = memoized_key_values
78
-
79
- non_memoized_keys = keys.reject do |key|
80
- if mkv.has_key?(key)
81
- memoized_keys << key if IdentityCache.logger.debug?
82
- hit = mkv[key]
83
- hash[key] = hit unless hit.nil?
84
- true
104
+ memo_miss_keys = EMPTY_ARRAY
105
+ cache_miss_keys = EMPTY_ARRAY
106
+
107
+ result = ActiveSupport::Notifications.instrument('cache_fetch_multi.identity_cache') do |payload|
108
+ payload[:resolve_miss_time] = 0.0
109
+
110
+ result = fetch_multi_memoized(keys) do |non_memoized_keys|
111
+ memo_miss_keys = non_memoized_keys
112
+ @cache_fetcher.fetch_multi(non_memoized_keys) do |missing_keys|
113
+ cache_miss_keys = missing_keys
114
+ instrument_duration(payload, :resolve_miss_time) do
115
+ yield missing_keys
116
+ end
85
117
  end
86
118
  end
87
119
 
88
- unless non_memoized_keys.empty?
89
- results = @cache_fetcher.fetch_multi(non_memoized_keys) do |missing_keys|
90
- missed_keys.concat(missing_keys) if IdentityCache.logger.debug?
91
- yield missing_keys
92
- end
93
- mkv.merge! results
94
- hash.merge! results
95
- end
96
- hash
97
- else
98
- @cache_fetcher.fetch_multi(keys) do |missing_keys|
99
- missed_keys.concat(missing_keys) if IdentityCache.logger.debug?
100
- yield missing_keys
101
- end
120
+ set_instrumentation_payload(
121
+ payload,
122
+ num_keys: keys.length,
123
+ memo_misses: memo_miss_keys.length,
124
+ cache_misses: cache_miss_keys.length
125
+ )
126
+ result
102
127
  end
103
128
 
104
- log_multi_result(memoized_keys, keys - missed_keys - memoized_keys, missed_keys) if IdentityCache.logger.debug?
129
+ log_multi_result(keys, memo_miss_keys, cache_miss_keys)
105
130
 
106
131
  result
107
132
  end
108
133
 
109
134
  def clear
110
- clear_memoization
111
- @cache_fetcher.clear
135
+ ActiveSupport::Notifications.instrument('cache_clear.identity_cache') do
136
+ clear_memoization
137
+ @cache_fetcher.clear
138
+ end
112
139
  end
113
140
 
114
141
  private
115
142
 
143
+ EMPTY_ARRAY = [].freeze
144
+ private_constant :EMPTY_ARRAY
145
+
146
+ def set_instrumentation_payload(payload, num_keys:, memo_misses:, cache_misses:)
147
+ payload[:memoizing] = memoizing?
148
+ payload[:memo_hits] = num_keys - memo_misses
149
+ payload[:cache_hits] = memo_misses - cache_misses
150
+ payload[:cache_misses] = cache_misses
151
+ end
152
+
153
+ def fetch_memoized(key)
154
+ return yield unless memoizing?
155
+ if memoized_key_values.key?(key)
156
+ return memoized_key_values[key]
157
+ end
158
+ memoized_key_values[key] = yield
159
+ end
160
+
161
+ def fetch_multi_memoized(keys)
162
+ return yield keys unless memoizing?
163
+
164
+ result = {}
165
+ missing_keys = keys.reject do |key|
166
+ if memoized_key_values.key?(key)
167
+ result[key] = memoized_key_values[key]
168
+ true
169
+ end
170
+ end
171
+
172
+ unless missing_keys.empty?
173
+ block_result = yield missing_keys
174
+ memoized_key_values.merge!(block_result)
175
+ result.merge!(block_result)
176
+ end
177
+
178
+ result
179
+ end
180
+
181
+ def instrument_duration(payload, key)
182
+ value = nil
183
+ payload[key] += Benchmark.realtime do
184
+ value = yield
185
+ end
186
+ value
187
+ end
188
+
116
189
  def clear_memoization
117
190
  @key_value_maps.delete(Thread.current)
118
191
  end
119
192
 
120
193
  def memoizing?
121
- Thread.current[:memoizing_idc]
194
+ !!Thread.current[:memoizing_idc]
122
195
  end
123
196
 
124
- def log_multi_result(memoized_keys, backend_keys, missed_keys)
125
- memoized_keys.each {|k| IdentityCache.logger.debug "[IdentityCache] (memoized) cache hit for #{k} (multi)" }
126
- backend_keys.each {|k| IdentityCache.logger.debug "[IdentityCache] (backend) cache hit for #{k} (multi)" }
127
- missed_keys.each {|k| IdentityCache.logger.debug "[IdentityCache] cache miss for #{k} (multi)" }
197
+ def log_multi_result(keys, memo_miss_keys, cache_miss_keys)
198
+ return unless IdentityCache.logger.debug?
199
+
200
+ memoized_keys = keys - memo_miss_keys
201
+ cache_hit_keys = memo_miss_keys - cache_miss_keys
202
+ missed_keys = cache_miss_keys
203
+
204
+ memoized_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (memoized) cache hit for #{k} (multi)") }
205
+ cache_hit_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (backend) cache hit for #{k} (multi)") }
206
+ missed_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] cache miss for #{k} (multi)") }
128
207
  end
129
208
  end
130
209
  end