identity_cache 0.5.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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