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,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