identity_cache 0.5.1 → 1.0.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 (91) hide show
  1. checksums.yaml +5 -5
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/ci.yml +26 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +5 -0
  6. data/.travis.yml +24 -9
  7. data/CHANGELOG.md +21 -0
  8. data/Gemfile +5 -1
  9. data/README.md +28 -26
  10. data/Rakefile +14 -5
  11. data/dev.yml +9 -16
  12. data/gemfiles/Gemfile.latest-release +6 -0
  13. data/gemfiles/Gemfile.rails-edge +6 -0
  14. data/gemfiles/Gemfile.rails52 +6 -0
  15. data/identity_cache.gemspec +26 -10
  16. data/lib/identity_cache.rb +49 -46
  17. data/lib/identity_cache/belongs_to_caching.rb +12 -40
  18. data/lib/identity_cache/cache_fetcher.rb +6 -5
  19. data/lib/identity_cache/cache_hash.rb +2 -2
  20. data/lib/identity_cache/cache_invalidation.rb +4 -11
  21. data/lib/identity_cache/cache_key_generation.rb +17 -65
  22. data/lib/identity_cache/cache_key_loader.rb +128 -0
  23. data/lib/identity_cache/cached.rb +7 -0
  24. data/lib/identity_cache/cached/association.rb +87 -0
  25. data/lib/identity_cache/cached/attribute.rb +123 -0
  26. data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
  27. data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
  28. data/lib/identity_cache/cached/belongs_to.rb +93 -0
  29. data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
  30. data/lib/identity_cache/cached/prefetcher.rb +51 -0
  31. data/lib/identity_cache/cached/primary_index.rb +97 -0
  32. data/lib/identity_cache/cached/recursive/association.rb +68 -0
  33. data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
  34. data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
  35. data/lib/identity_cache/cached/reference/association.rb +16 -0
  36. data/lib/identity_cache/cached/reference/has_many.rb +105 -0
  37. data/lib/identity_cache/cached/reference/has_one.rb +100 -0
  38. data/lib/identity_cache/configuration_dsl.rb +53 -215
  39. data/lib/identity_cache/encoder.rb +95 -0
  40. data/lib/identity_cache/expiry_hook.rb +36 -0
  41. data/lib/identity_cache/fallback_fetcher.rb +2 -1
  42. data/lib/identity_cache/load_strategy/eager.rb +28 -0
  43. data/lib/identity_cache/load_strategy/lazy.rb +71 -0
  44. data/lib/identity_cache/load_strategy/load_request.rb +20 -0
  45. data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
  46. data/lib/identity_cache/memoized_cache_proxy.rb +127 -58
  47. data/lib/identity_cache/parent_model_expiration.rb +45 -11
  48. data/lib/identity_cache/query_api.rb +128 -394
  49. data/lib/identity_cache/railtie.rb +8 -0
  50. data/lib/identity_cache/record_not_found.rb +6 -0
  51. data/lib/identity_cache/should_use_cache.rb +1 -0
  52. data/lib/identity_cache/version.rb +3 -2
  53. data/lib/identity_cache/with_primary_index.rb +136 -0
  54. data/lib/identity_cache/without_primary_index.rb +24 -3
  55. data/performance/cache_runner.rb +28 -34
  56. data/performance/cpu.rb +3 -2
  57. data/performance/externals.rb +4 -3
  58. data/performance/profile.rb +6 -5
  59. data/railgun.yml +16 -0
  60. metadata +44 -73
  61. data/Gemfile.rails42 +0 -6
  62. data/Gemfile.rails50 +0 -6
  63. data/test/attribute_cache_test.rb +0 -110
  64. data/test/cache_fetch_includes_test.rb +0 -46
  65. data/test/cache_hash_test.rb +0 -14
  66. data/test/cache_invalidation_test.rb +0 -139
  67. data/test/deeply_nested_associated_record_test.rb +0 -19
  68. data/test/denormalized_has_many_test.rb +0 -214
  69. data/test/denormalized_has_one_test.rb +0 -160
  70. data/test/fetch_multi_test.rb +0 -308
  71. data/test/fetch_test.rb +0 -258
  72. data/test/fixtures/serialized_record.mysql2 +0 -0
  73. data/test/fixtures/serialized_record.postgresql +0 -0
  74. data/test/helpers/active_record_objects.rb +0 -106
  75. data/test/helpers/database_connection.rb +0 -72
  76. data/test/helpers/serialization_format.rb +0 -51
  77. data/test/helpers/update_serialization_format.rb +0 -27
  78. data/test/identity_cache_test.rb +0 -29
  79. data/test/index_cache_test.rb +0 -161
  80. data/test/memoized_attributes_test.rb +0 -59
  81. data/test/memoized_cache_proxy_test.rb +0 -107
  82. data/test/normalized_belongs_to_test.rb +0 -107
  83. data/test/normalized_has_many_test.rb +0 -231
  84. data/test/normalized_has_one_test.rb +0 -9
  85. data/test/prefetch_associations_test.rb +0 -379
  86. data/test/readonly_test.rb +0 -109
  87. data/test/recursive_denormalized_has_many_test.rb +0 -131
  88. data/test/save_test.rb +0 -82
  89. data/test/schema_change_test.rb +0 -112
  90. data/test/serialization_format_change_test.rb +0 -16
  91. data/test/test_helper.rb +0 -140
@@ -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
@@ -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,20 @@ 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)
13
15
  if cache_adaptor.respond_to?(:cas) && cache_adaptor.respond_to?(:cas_multi)
14
16
  @cache_fetcher = CacheFetcher.new(cache_adaptor)
15
17
  else
18
+ case cache_adaptor
19
+ when ActiveSupport::Cache::MemoryStore, ActiveSupport::Cache::NullStore
20
+ # no need for CAS support
21
+ else
22
+ warn("[IdentityCache] Missing CAS support in cache backend #{cache_adaptor.class} "\
23
+ "which is needed for cache consistency")
24
+ end
16
25
  @cache_fetcher = FallbackFetcher.new(cache_adaptor)
17
26
  end
18
27
  end
@@ -21,7 +30,7 @@ module IdentityCache
21
30
  @key_value_maps[Thread.current]
22
31
  end
23
32
 
24
- def with_memoization(&block)
33
+ def with_memoization
25
34
  Thread.current[:memoizing_idc] = true
26
35
  yield
27
36
  ensure
@@ -30,101 +39,161 @@ module IdentityCache
30
39
  end
31
40
 
32
41
  def write(key, value)
33
- memoized_key_values[key] = value if memoizing?
34
- @cache_fetcher.write(key, value)
42
+ memoizing = memoizing?
43
+ ActiveSupport::Notifications.instrument('cache_write.identity_cache', memoizing: memoizing) do
44
+ memoized_key_values[key] = value if memoizing
45
+ @cache_fetcher.write(key, value)
46
+ end
35
47
  end
36
48
 
37
49
  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
50
+ memoizing = memoizing?
51
+ ActiveSupport::Notifications.instrument('cache_delete.identity_cache', memoizing: memoizing) do
52
+ memoized_key_values.delete(key) if memoizing
53
+ if (result = @cache_fetcher.delete(key))
54
+ IdentityCache.logger.debug { "[IdentityCache] delete recorded for #{key}" }
55
+ else
56
+ IdentityCache.logger.error { "[IdentityCache] delete failed for #{key}" }
57
+ end
58
+ result
59
+ end
42
60
  end
43
61
 
44
62
  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
63
+ memo_misses = 0
64
+ cache_misses = 0
65
+
66
+ value = ActiveSupport::Notifications.instrument('cache_fetch.identity_cache') do |payload|
67
+ payload[:resolve_miss_time] = 0.0
68
+
69
+ value = fetch_memoized(key) do
70
+ memo_misses = 1
71
+ @cache_fetcher.fetch(key) do
72
+ cache_misses = 1
73
+ instrument_duration(payload, :resolve_miss_time) do
74
+ yield
75
+ end
54
76
  end
55
77
  end
56
- else
57
- @cache_fetcher.fetch(key) do
58
- missed = true
59
- yield
60
- end
78
+ set_instrumentation_payload(payload, num_keys: 1, memo_misses: memo_misses, cache_misses: cache_misses)
79
+ value
61
80
  end
62
81
 
63
- if missed
82
+ if cache_misses > 0
64
83
  IdentityCache.logger.debug { "[IdentityCache] cache miss for #{key}" }
65
84
  else
66
- IdentityCache.logger.debug { "[IdentityCache] #{ used_cache_backend ? '(cache_backend)' : '(memoized)' } cache hit for #{key}" }
85
+ IdentityCache.logger.debug do
86
+ "[IdentityCache] #{memo_misses > 0 ? '(cache_backend)' : '(memoized)'} cache hit for #{key}"
87
+ end
67
88
  end
68
89
 
69
90
  value
70
91
  end
71
92
 
72
93
  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
94
+ memo_miss_keys = EMPTY_ARRAY
95
+ cache_miss_keys = EMPTY_ARRAY
96
+
97
+ result = ActiveSupport::Notifications.instrument('cache_fetch_multi.identity_cache') do |payload|
98
+ payload[:resolve_miss_time] = 0.0
99
+
100
+ result = fetch_multi_memoized(keys) do |non_memoized_keys|
101
+ memo_miss_keys = non_memoized_keys
102
+ @cache_fetcher.fetch_multi(non_memoized_keys) do |missing_keys|
103
+ cache_miss_keys = missing_keys
104
+ instrument_duration(payload, :resolve_miss_time) do
105
+ yield missing_keys
106
+ end
85
107
  end
86
108
  end
87
109
 
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
110
+ set_instrumentation_payload(
111
+ payload,
112
+ num_keys: keys.length,
113
+ memo_misses: memo_miss_keys.length,
114
+ cache_misses: cache_miss_keys.length
115
+ )
116
+ result
102
117
  end
103
118
 
104
- log_multi_result(memoized_keys, keys - missed_keys - memoized_keys, missed_keys) if IdentityCache.logger.debug?
119
+ log_multi_result(keys, memo_miss_keys, cache_miss_keys)
105
120
 
106
121
  result
107
122
  end
108
123
 
109
124
  def clear
110
- clear_memoization
111
- @cache_fetcher.clear
125
+ ActiveSupport::Notifications.instrument('cache_clear.identity_cache') do
126
+ clear_memoization
127
+ @cache_fetcher.clear
128
+ end
112
129
  end
113
130
 
114
131
  private
115
132
 
133
+ EMPTY_ARRAY = [].freeze
134
+ private_constant :EMPTY_ARRAY
135
+
136
+ def set_instrumentation_payload(payload, num_keys:, memo_misses:, cache_misses:)
137
+ payload[:memoizing] = memoizing?
138
+ payload[:memo_hits] = num_keys - memo_misses
139
+ payload[:cache_hits] = memo_misses - cache_misses
140
+ payload[:cache_misses] = cache_misses
141
+ end
142
+
143
+ def fetch_memoized(key)
144
+ return yield unless memoizing?
145
+ if memoized_key_values.key?(key)
146
+ return memoized_key_values[key]
147
+ end
148
+ memoized_key_values[key] = yield
149
+ end
150
+
151
+ def fetch_multi_memoized(keys)
152
+ return yield keys unless memoizing?
153
+
154
+ result = {}
155
+ missing_keys = keys.reject do |key|
156
+ if memoized_key_values.key?(key)
157
+ result[key] = memoized_key_values[key]
158
+ true
159
+ end
160
+ end
161
+
162
+ unless missing_keys.empty?
163
+ block_result = yield missing_keys
164
+ memoized_key_values.merge!(block_result)
165
+ result.merge!(block_result)
166
+ end
167
+
168
+ result
169
+ end
170
+
171
+ def instrument_duration(payload, key)
172
+ value = nil
173
+ payload[key] += Benchmark.realtime do
174
+ value = yield
175
+ end
176
+ value
177
+ end
178
+
116
179
  def clear_memoization
117
180
  @key_value_maps.delete(Thread.current)
118
181
  end
119
182
 
120
183
  def memoizing?
121
- Thread.current[:memoizing_idc]
184
+ !!Thread.current[:memoizing_idc]
122
185
  end
123
186
 
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)" }
187
+ def log_multi_result(keys, memo_miss_keys, cache_miss_keys)
188
+ IdentityCache.logger.debug do
189
+ memoized_keys = keys - memo_miss_keys
190
+ cache_hit_keys = memo_miss_keys - cache_miss_keys
191
+ missed_keys = cache_miss_keys
192
+
193
+ memoized_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (memoized) cache hit for #{k} (multi)") }
194
+ cache_hit_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (backend) cache hit for #{k} (multi)") }
195
+ missed_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] cache miss for #{k} (multi)") }
196
+ end
128
197
  end
129
198
  end
130
199
  end
@@ -1,17 +1,50 @@
1
+ # frozen_string_literal: true
1
2
  module IdentityCache
2
3
  module ParentModelExpiration # :nodoc:
3
4
  extend ActiveSupport::Concern
4
5
 
5
- included do |base|
6
- base.class_attribute :parent_expiration_entries
7
- base.parent_expiration_entries = Hash.new{ |hash, key| hash[key] = [] }
6
+ class << self
7
+ def add_parent_expiry_hook(cached_association)
8
+ name = cached_association.reflection.class_name.demodulize
9
+ lazy_hooks[name] << ExpiryHook.new(cached_association)
10
+ end
11
+
12
+ def install_all_pending_parent_expiry_hooks
13
+ until lazy_hooks.empty?
14
+ lazy_hooks.keys.each do |name|
15
+ if (hooks = lazy_hooks.delete(name))
16
+ hooks.each(&:install)
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ def install_pending_parent_expiry_hooks(model)
23
+ name = model.name.demodulize
24
+ if (hooks = lazy_hooks.delete(name))
25
+ hooks.each(&:install)
26
+ end
27
+ end
28
+
29
+ private
30
+
31
+ def lazy_hooks
32
+ @lazy_hooks ||= Hash.new { |hash, key| hash[key] = [] }
33
+ end
34
+ end
35
+
36
+ included do
37
+ class_attribute(:parent_expiration_entries)
38
+ self.parent_expiration_entries = Hash.new { |hash, key| hash[key] = [] }
39
+ after_commit(:expire_parent_caches)
8
40
  end
9
41
 
10
42
  def expire_parent_caches
11
- parents_to_expire = {}
43
+ ParentModelExpiration.install_pending_parent_expiry_hooks(cached_model)
44
+ parents_to_expire = Set.new
12
45
  add_parents_to_cache_expiry_set(parents_to_expire)
13
- parents_to_expire.each_value do |parent|
14
- parent.send(:expire_primary_index)
46
+ parents_to_expire.each do |parent|
47
+ parent.expire_primary_index if parent.class.primary_cache_index_enabled
15
48
  end
16
49
  end
17
50
 
@@ -22,9 +55,7 @@ module IdentityCache
22
55
  end
23
56
 
24
57
  def add_record_to_cache_expiry_set(parents_to_expire, record)
25
- key = record.primary_cache_index_key
26
- unless parents_to_expire[key]
27
- parents_to_expire[key] = record
58
+ if parents_to_expire.add?(record)
28
59
  record.add_parents_to_cache_expiry_set(parents_to_expire)
29
60
  end
30
61
  end
@@ -52,11 +83,12 @@ module IdentityCache
52
83
  end
53
84
 
54
85
  cached_associations.each do |parent_class, only_on_foreign_key_change|
55
- if new_parent && new_parent.is_a?(parent_class) && should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
86
+ if new_parent&.is_a?(parent_class) &&
87
+ should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
56
88
  add_record_to_cache_expiry_set(parents_to_expire, new_parent)
57
89
  end
58
90
 
59
- if old_parent && old_parent.is_a?(parent_class)
91
+ if old_parent&.is_a?(parent_class)
60
92
  add_record_to_cache_expiry_set(parents_to_expire, old_parent)
61
93
  end
62
94
  end
@@ -70,4 +102,6 @@ module IdentityCache
70
102
  end
71
103
  end
72
104
  end
105
+
106
+ private_constant :ParentModelExpiration
73
107
  end