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,8 +1,32 @@
1
+ # frozen_string_literal: true
1
2
  require 'active_record'
2
3
  require 'active_support/core_ext/module/attribute_accessors'
3
4
  require 'ar_transaction_changes'
4
5
 
5
6
  require "identity_cache/version"
7
+ require "identity_cache/record_not_found"
8
+ require "identity_cache/encoder"
9
+ require "identity_cache/cache_key_loader"
10
+ require "identity_cache/load_strategy/load_request"
11
+ require "identity_cache/load_strategy/multi_load_request"
12
+ require "identity_cache/load_strategy/eager"
13
+ require "identity_cache/load_strategy/lazy"
14
+ require "identity_cache/cached"
15
+ require "identity_cache/cached/prefetcher"
16
+ require "identity_cache/cached/embedded_fetching"
17
+ require "identity_cache/cached/association"
18
+ require "identity_cache/cached/attribute"
19
+ require "identity_cache/cached/attribute_by_one"
20
+ require "identity_cache/cached/attribute_by_multi"
21
+ require "identity_cache/cached/belongs_to"
22
+ require "identity_cache/cached/primary_index"
23
+ require "identity_cache/cached/recursive/association"
24
+ require "identity_cache/cached/recursive/has_one"
25
+ require "identity_cache/cached/recursive/has_many"
26
+ require "identity_cache/cached/reference/association"
27
+ require "identity_cache/cached/reference/has_one"
28
+ require "identity_cache/cached/reference/has_many"
29
+ require "identity_cache/expiry_hook"
6
30
  require 'identity_cache/memoized_cache_proxy'
7
31
  require 'identity_cache/belongs_to_caching'
8
32
  require 'identity_cache/cache_key_generation'
@@ -15,18 +39,12 @@ require "identity_cache/cache_invalidation"
15
39
  require "identity_cache/cache_fetcher"
16
40
  require "identity_cache/fallback_fetcher"
17
41
  require 'identity_cache/without_primary_index'
42
+ require 'identity_cache/with_primary_index'
18
43
 
19
44
  module IdentityCache
20
45
  extend ActiveSupport::Concern
21
46
 
22
- include ArTransactionChanges
23
- include IdentityCache::BelongsToCaching
24
- include IdentityCache::CacheKeyGeneration
25
- include IdentityCache::ConfigurationDSL
26
- include IdentityCache::QueryAPI
27
- include IdentityCache::CacheInvalidation
28
- include IdentityCache::ShouldUseCache
29
- include IdentityCache::ParentModelExpiration
47
+ include WithPrimaryIndex
30
48
 
31
49
  CACHED_NIL = :idc_cached_nil
32
50
  BATCH_SIZE = 1000
@@ -35,40 +53,28 @@ module IdentityCache
35
53
 
36
54
  class AlreadyIncludedError < StandardError; end
37
55
  class AssociationError < StandardError; end
38
- class InverseAssociationError < StandardError
39
- def initialize
40
- super "Inverse name for association could not be determined. Please use the :inverse_name option to specify the inverse association name for this cache."
41
- end
42
- end
56
+ class InverseAssociationError < StandardError; end
43
57
  class UnsupportedScopeError < StandardError; end
44
58
  class UnsupportedAssociationError < StandardError; end
45
59
  class DerivedModelError < StandardError; end
46
60
 
61
+ mattr_accessor :cache_namespace
62
+ self.cache_namespace = "IDC:#{CACHE_VERSION}:"
63
+
64
+ # Fetched records are not read-only and this could sometimes prevent IDC from
65
+ # reflecting what's truly in the database when fetch_read_only_records is false.
66
+ # When set to true, it will only return read-only records when cache is used.
67
+ mattr_accessor :fetch_read_only_records
68
+ self.fetch_read_only_records = true
69
+
47
70
  class << self
48
71
  include IdentityCache::CacheHash
49
72
 
50
73
  attr_accessor :readonly
51
74
  attr_writer :logger
52
75
 
53
- mattr_accessor :cache_namespace
54
- self.cache_namespace = "IDC:#{CACHE_VERSION}:".freeze
55
-
56
- # Inverse active record associations are set when loading embedded
57
- # cache_has_many associations from the cache when never_set_inverse_association
58
- # is false. When set to true, it will only set the inverse cached association.
59
- mattr_accessor :never_set_inverse_association
60
- self.never_set_inverse_association = true
61
-
62
- # Fetched records are not read-only and this could sometimes prevent IDC from
63
- # reflecting what's truly in the database when fetch_read_only_records is false.
64
- # When set to true, it will only return read-only records when cache is used.
65
- mattr_accessor :fetch_read_only_records
66
- self.fetch_read_only_records = true
67
-
68
- def included(base) #:nodoc:
69
- raise AlreadyIncludedError if base.respond_to?(:cached_model)
70
- base.class_attribute :cached_model
71
- base.cached_model = base
76
+ def append_features(base) #:nodoc:
77
+ raise AlreadyIncludedError if base.include?(IdentityCache)
72
78
  super
73
79
  end
74
80
 
@@ -133,12 +139,12 @@ module IdentityCache
133
139
  # +keys+ A collection or array of key strings
134
140
  def fetch_multi(*keys)
135
141
  keys.flatten!(1)
136
- return {} if keys.size == 0
142
+ return {} if keys.empty?
137
143
 
138
144
  result = if should_use_cache?
139
145
  fetch_in_batches(keys.uniq) do |missed_keys|
140
146
  results = yield missed_keys
141
- results.map {|e| map_cached_nil_for e }
147
+ results.map { |e| map_cached_nil_for e }
142
148
  end
143
149
  else
144
150
  results = yield keys
@@ -152,29 +158,26 @@ module IdentityCache
152
158
  result
153
159
  end
154
160
 
155
- def with_never_set_inverse_association(value = true)
156
- old_value = self.never_set_inverse_association
157
- self.never_set_inverse_association = value
158
- yield
159
- ensure
160
- self.never_set_inverse_association = old_value
161
- end
162
-
163
-
164
161
  def with_fetch_read_only_records(value = true)
165
- old_value = self.fetch_read_only_records
162
+ old_value = fetch_read_only_records
166
163
  self.fetch_read_only_records = value
167
164
  yield
168
165
  ensure
169
166
  self.fetch_read_only_records = old_value
170
167
  end
171
168
 
169
+ def eager_load!
170
+ ParentModelExpiration.install_all_pending_parent_expiry_hooks
171
+ end
172
+
172
173
  private
173
174
 
174
175
  def fetch_in_batches(keys)
175
- keys.each_slice(BATCH_SIZE).each_with_object Hash.new do |slice, result|
176
- result.merge! cache.fetch_multi(*slice) {|missed_keys| yield missed_keys }
176
+ keys.each_slice(BATCH_SIZE).each_with_object({}) do |slice, result|
177
+ result.merge!(cache.fetch_multi(*slice) { |missed_keys| yield missed_keys })
177
178
  end
178
179
  end
179
180
  end
180
181
  end
182
+
183
+ require 'identity_cache/railtie' if defined?(Rails)
@@ -1,59 +1,31 @@
1
+ # frozen_string_literal: true
1
2
  module IdentityCache
2
3
  module BelongsToCaching
3
4
  extend ActiveSupport::Concern
4
5
 
5
6
  included do |base|
6
- base.class_attribute :cached_belongs_tos
7
+ base.class_attribute(:cached_belongs_tos)
7
8
  base.cached_belongs_tos = {}
8
9
  end
9
10
 
10
11
  module ClassMethods
11
- def cache_belongs_to(association, options = {})
12
+ def cache_belongs_to(association)
12
13
  ensure_base_model
13
- raise NotImplementedError if options[:embed]
14
14
 
15
- unless association_reflection = reflect_on_association(association)
15
+ unless (reflection = reflect_on_association(association))
16
16
  raise AssociationError, "Association named '#{association}' was not found on #{self.class}"
17
17
  end
18
18
 
19
- options = {}
20
- self.cached_belongs_tos[association] = options
21
-
22
- options[:embed] = false
23
- options[:cached_accessor_name] = "fetch_#{association}"
24
- options[:records_variable_name] = "cached_#{association}"
25
- options[:association_reflection] = association_reflection
26
- options[:prepopulate_method_name] = "prepopulate_fetched_#{association}"
27
-
28
- build_normalized_belongs_to_cache(association, options)
29
- end
30
-
31
- private
19
+ if reflection.scope
20
+ raise(
21
+ UnsupportedAssociationError,
22
+ "caching association #{self}.#{association} is scoped which isn't supported"
23
+ )
24
+ end
32
25
 
33
- def build_normalized_belongs_to_cache(association, options)
34
- foreign_key = options[:association_reflection].foreign_key
35
- self.class_eval(<<-CODE, __FILE__, __LINE__ + 1)
36
- def #{options[:cached_accessor_name]}
37
- association_klass = association(:#{association}).klass
38
- if association_klass.should_use_cache? && #{foreign_key}.present? && !association(:#{association}).loaded?
39
- if instance_variable_defined?(:@#{options[:records_variable_name]})
40
- @#{options[:records_variable_name]}
41
- else
42
- @#{options[:records_variable_name]} = association_klass.fetch_by_id(#{foreign_key})
43
- end
44
- else
45
- if IdentityCache.fetch_read_only_records && association_klass.should_use_cache?
46
- readonly_copy(association(:#{association}).load_target)
47
- else
48
- #{association}
49
- end
50
- end
51
- end
26
+ cached_belongs_to = Cached::BelongsTo.new(association, reflection: reflection)
52
27
 
53
- def #{options[:prepopulate_method_name]}(record)
54
- @#{options[:records_variable_name]} = record
55
- end
56
- CODE
28
+ cached_belongs_tos[association] = cached_belongs_to.tap(&:build)
57
29
  end
58
30
  end
59
31
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module IdentityCache
2
3
  class CacheFetcher
3
4
  attr_accessor :cache_backend
@@ -11,7 +12,7 @@ module IdentityCache
11
12
  end
12
13
 
13
14
  def delete(key)
14
- @cache_backend.write(key, IdentityCache::DELETED, :expires_in => IdentityCache::DELETED_TTL.seconds)
15
+ @cache_backend.write(key, IdentityCache::DELETED, expires_in: IdentityCache::DELETED_TTL.seconds)
15
16
  end
16
17
 
17
18
  def clear
@@ -49,8 +50,8 @@ module IdentityCache
49
50
  def cas_multi(keys)
50
51
  result = nil
51
52
  @cache_backend.cas_multi(*keys) do |results|
52
- deleted = results.select {|_, v| IdentityCache::DELETED == v }
53
- results.reject! {|_, v| IdentityCache::DELETED == v }
53
+ deleted = results.select { |_, v| IdentityCache::DELETED == v }
54
+ results.reject! { |_, v| IdentityCache::DELETED == v }
54
55
 
55
56
  result = results
56
57
  updates = {}
@@ -77,11 +78,11 @@ module IdentityCache
77
78
  def add_multi(keys)
78
79
  values = yield keys
79
80
  result = Hash[keys.zip(values)]
80
- result.each {|k, v| add(k, v) }
81
+ result.each { |k, v| add(k, v) }
81
82
  end
82
83
 
83
84
  def add(key, value)
84
- @cache_backend.write(key, value, :unless_exist => true) if IdentityCache.should_fill_cache?
85
+ @cache_backend.write(key, value, unless_exist: true) if IdentityCache.should_fill_cache?
85
86
  end
86
87
  end
87
88
  end
@@ -1,9 +1,10 @@
1
+ # frozen_string_literal: true
1
2
  # Use CityHash for fast hashing if it is available; use Digest::MD5 otherwise
2
3
  begin
3
4
  require 'cityhash'
4
5
  rescue LoadError
5
6
  unless RUBY_PLATFORM == 'java'
6
- warn <<-NOTICE
7
+ warn(<<-NOTICE)
7
8
  ** Notice: CityHash was not loaded. **
8
9
 
9
10
  For optimal performance, use of the cityhash gem is recommended.
@@ -19,7 +20,6 @@ end
19
20
 
20
21
  module IdentityCache
21
22
  module CacheHash
22
-
23
23
  if defined?(CityHash)
24
24
 
25
25
  def memcache_hash(key) #:nodoc:
@@ -1,7 +1,7 @@
1
+ # frozen_string_literal: true
1
2
  module IdentityCache
2
3
  module CacheInvalidation
3
-
4
- CACHE_KEY_NAMES = [:ids_variable_name, :records_variable_name]
4
+ CACHE_KEY_NAMES = [:ids_variable_name, :id_variable_name, :records_variable_name]
5
5
 
6
6
  def reload(*)
7
7
  clear_cached_associations
@@ -11,15 +11,8 @@ module IdentityCache
11
11
  private
12
12
 
13
13
  def clear_cached_associations
14
- self.class.send(:all_cached_associations).each do |_, data|
15
- CACHE_KEY_NAMES.each do |key|
16
- if data[key]
17
- instance_variable_name = "@#{data[key]}"
18
- if instance_variable_defined?(instance_variable_name)
19
- remove_instance_variable(instance_variable_name)
20
- end
21
- end
22
- end
14
+ self.class.send(:all_cached_associations).each_value do |association|
15
+ association.clear(self)
23
16
  end
24
17
  end
25
18
  end
@@ -1,88 +1,40 @@
1
+ # frozen_string_literal: true
1
2
  module IdentityCache
2
3
  module CacheKeyGeneration
3
4
  extend ActiveSupport::Concern
4
- DEFAULT_NAMESPACE = "IDC:#{CACHE_VERSION}:".freeze
5
+ DEFAULT_NAMESPACE = "IDC:#{CACHE_VERSION}:"
5
6
 
6
7
  def self.schema_to_string(columns)
7
- columns.sort_by(&:name).map{|c| "#{c.name}:#{c.type}"}.join(',')
8
+ columns.sort_by(&:name).map { |c| "#{c.name}:#{c.type}" }.join(',')
8
9
  end
9
10
 
10
- def self.denormalized_schema_hash(klass)
11
- schema_string = schema_to_string(klass.columns)
12
- if klass.include?(IdentityCache)
13
- klass.send(:all_cached_associations).sort.each do |name, options|
11
+ def self.denormalized_schema_string(klass)
12
+ schema_to_string(klass.columns).tap do |schema_string|
13
+ klass.send(:all_cached_associations).sort.each do |name, association|
14
14
  klass.send(:check_association_scope, name)
15
- case options[:embed]
16
- when true
17
- schema_string << ",#{name}:(#{denormalized_schema_hash(options[:association_reflection].klass)})"
18
- when :ids
15
+ association.validate if association.embedded?
16
+ case association
17
+ when Cached::Recursive::Association
18
+ schema_string << ",#{name}:(#{denormalized_schema_hash(association.reflection.klass)})"
19
+ when Cached::Reference::HasMany
19
20
  schema_string << ",#{name}:ids"
21
+ when Cached::Reference::HasOne
22
+ schema_string << ",#{name}:id"
20
23
  end
21
24
  end
22
25
  end
26
+ end
27
+
28
+ def self.denormalized_schema_hash(klass)
29
+ schema_string = denormalized_schema_string(klass)
23
30
  IdentityCache.memcache_hash(schema_string)
24
31
  end
25
32
 
26
33
  module ClassMethods
27
- def rails_cache_key(id)
28
- "#{prefixed_rails_cache_key}#{id}"
29
- end
30
-
31
- def rails_cache_key_prefix
32
- @rails_cache_key_prefix ||= IdentityCache::CacheKeyGeneration.denormalized_schema_hash(self)
33
- end
34
-
35
- def prefixed_rails_cache_key
36
- "#{rails_cache_key_namespace}blob:#{base_class.name}:#{rails_cache_key_prefix}:"
37
- end
38
-
39
- def rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values, unique)
40
- unique_indicator = unique ? '' : 's'
41
- "#{rails_cache_key_namespace}" \
42
- "attr#{unique_indicator}" \
43
- ":#{base_class.name}" \
44
- ":#{attribute}" \
45
- ":#{rails_cache_string_for_fields_and_values(fields, values)}"
46
- end
47
-
48
34
  def rails_cache_key_namespace
49
35
  ns = IdentityCache.cache_namespace
50
36
  ns.is_a?(Proc) ? ns.call(self) : ns
51
37
  end
52
-
53
- private
54
- def rails_cache_string_for_fields_and_values(fields, values)
55
- "#{fields.join('/')}:#{IdentityCache.memcache_hash(values.join('/'))}"
56
- end
57
- end
58
-
59
- def primary_cache_index_key # :nodoc:
60
- self.class.rails_cache_key(id)
61
- end
62
-
63
- def attribute_cache_key_for_attribute_and_current_values(attribute, fields, unique) # :nodoc:
64
- self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, current_values_for_fields(fields), unique)
65
- end
66
-
67
- def attribute_cache_key_for_attribute_and_previous_values(attribute, fields, unique) # :nodoc:
68
- self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, old_values_for_fields(fields), unique)
69
- end
70
-
71
- def current_values_for_fields(fields) # :nodoc:
72
- fields.collect {|field| self.send(field)}
73
- end
74
-
75
- def old_values_for_fields(fields) # :nodoc:
76
- fields.map do |field|
77
- field_string = field.to_s
78
- if destroyed? && transaction_changed_attributes.has_key?(field_string)
79
- transaction_changed_attributes[field_string]
80
- elsif persisted? && transaction_changed_attributes.has_key?(field_string)
81
- transaction_changed_attributes[field_string]
82
- else
83
- self.send(field)
84
- end
85
- end
86
38
  end
87
39
  end
88
40
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IdentityCache
4
+ # A generic cache key loader that supports different types of
5
+ # cache fetchers, each of which can use their own cache key
6
+ # format and have their own cache miss resolvers.
7
+ #
8
+ # Here is the interface of a cache fetcher in the
9
+ # [ruby-signature](https://github.com/ruby/ruby-signature)'s
10
+ # format.
11
+ #
12
+ # ```
13
+ # interface _CacheFetcher[DbKey, DbValue, CacheableValue]
14
+ # def cache_key: (DbKey) -> String
15
+ # def cache_encode: (DbValue) -> CacheableValue
16
+ # def cache_decode: (CacheableValue) -> DbValue
17
+ # def load_one_from_db: (DbKey) -> DbValue
18
+ # def load_multi_from_db: (Array[DbKey]) -> Hash[DbKey, DbValue]
19
+ # end
20
+ # ```
21
+ module CacheKeyLoader
22
+ class << self
23
+ # Load a single key for a cache fetcher.
24
+ #
25
+ # @param cache_fetcher [_CacheFetcher]
26
+ # @param db_key Reference to what to load from the database.
27
+ # @return The database value corresponding to the database key.
28
+ def load(cache_fetcher, db_key)
29
+ cache_key = cache_fetcher.cache_key(db_key)
30
+
31
+ db_value = nil
32
+
33
+ cache_value = IdentityCache.fetch(cache_key) do
34
+ db_value = cache_fetcher.load_one_from_db(db_key)
35
+ cache_fetcher.cache_encode(db_value)
36
+ end
37
+
38
+ db_value || cache_fetcher.cache_decode(cache_value)
39
+ end
40
+
41
+ # Load multiple keys for a cache fetcher.
42
+ #
43
+ # @param cache_fetcher [_CacheFetcher]
44
+ # @param db_key [Array] Reference to what to load from the database.
45
+ # @return [Hash] A hash mapping each database key to its corresponding value
46
+ def load_multi(cache_fetcher, db_keys)
47
+ load_batch(cache_fetcher => db_keys).fetch(cache_fetcher)
48
+ end
49
+
50
+ # Load multiple keys for multiple cache fetchers
51
+ def load_batch(cache_fetcher_to_db_keys_hash)
52
+ cache_key_to_db_key_hash = {}
53
+ cache_key_to_cache_fetcher_hash = {}
54
+
55
+ batch_load_result = {}
56
+
57
+ cache_fetcher_to_db_keys_hash.each do |cache_fetcher, db_keys|
58
+ if db_keys.empty?
59
+ batch_load_result[cache_fetcher] = {}
60
+ next
61
+ end
62
+ db_keys.each do |db_key|
63
+ cache_key = cache_fetcher.cache_key(db_key)
64
+ cache_key_to_db_key_hash[cache_key] = db_key
65
+ cache_key_to_cache_fetcher_hash[cache_key] = cache_fetcher
66
+ end
67
+ end
68
+
69
+ cache_keys = cache_key_to_db_key_hash.keys
70
+ cache_result = cache_fetch_multi(cache_keys) do |unresolved_cache_keys|
71
+ cache_fetcher_to_unresolved_keys_hash = unresolved_cache_keys.group_by do |cache_key|
72
+ cache_key_to_cache_fetcher_hash.fetch(cache_key)
73
+ end
74
+
75
+ resolve_miss_result = {}
76
+
77
+ db_keys_buffer = []
78
+ cache_fetcher_to_unresolved_keys_hash.each do |cache_fetcher, unresolved_cache_fetcher_keys|
79
+ batch_load_result[cache_fetcher] = resolve_multi_on_miss(cache_fetcher, unresolved_cache_fetcher_keys,
80
+ cache_key_to_db_key_hash, resolve_miss_result, db_keys_buffer: db_keys_buffer)
81
+ end
82
+
83
+ resolve_miss_result
84
+ end
85
+
86
+ cache_result.each do |cache_key, cache_value|
87
+ cache_fetcher = cache_key_to_cache_fetcher_hash.fetch(cache_key)
88
+ load_result = (batch_load_result[cache_fetcher] ||= {})
89
+
90
+ db_key = cache_key_to_db_key_hash.fetch(cache_key)
91
+ load_result[db_key] ||= cache_fetcher.cache_decode(cache_value)
92
+ end
93
+
94
+ batch_load_result
95
+ end
96
+
97
+ private
98
+
99
+ def cache_fetch_multi(cache_keys)
100
+ IdentityCache.fetch_multi(cache_keys) do |unresolved_cache_keys|
101
+ cache_key_to_cache_value_hash = yield unresolved_cache_keys
102
+ cache_key_to_cache_value_hash.fetch_values(*unresolved_cache_keys)
103
+ end
104
+ end
105
+
106
+ def resolve_multi_on_miss(
107
+ cache_fetcher, unresolved_cache_keys, cache_key_to_db_key_hash, resolve_miss_result,
108
+ db_keys_buffer: []
109
+ )
110
+ db_keys_buffer.clear
111
+ unresolved_cache_keys.each do |cache_key|
112
+ db_keys_buffer << cache_key_to_db_key_hash.fetch(cache_key)
113
+ end
114
+
115
+ load_result = cache_fetcher.load_multi_from_db(db_keys_buffer)
116
+
117
+ unresolved_cache_keys.each do |cache_key|
118
+ db_key = cache_key_to_db_key_hash.fetch(cache_key)
119
+ db_value = load_result[db_key]
120
+ resolve_miss_result[cache_key] = cache_fetcher.cache_encode(db_value)
121
+ end
122
+
123
+ load_result
124
+ end
125
+ end
126
+ end
127
+ private_constant :CacheKeyLoader
128
+ end