identity_cache 0.4.1 → 1.1.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 (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