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.
- checksums.yaml +5 -5
- data/.github/probots.yml +2 -0
- data/.github/workflows/ci.yml +26 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +5 -0
- data/.travis.yml +24 -9
- data/CHANGELOG.md +21 -0
- data/Gemfile +5 -1
- data/README.md +28 -26
- data/Rakefile +14 -5
- data/dev.yml +9 -16
- data/gemfiles/Gemfile.latest-release +6 -0
- data/gemfiles/Gemfile.rails-edge +6 -0
- data/gemfiles/Gemfile.rails52 +6 -0
- data/identity_cache.gemspec +26 -10
- data/lib/identity_cache.rb +49 -46
- data/lib/identity_cache/belongs_to_caching.rb +12 -40
- data/lib/identity_cache/cache_fetcher.rb +6 -5
- data/lib/identity_cache/cache_hash.rb +2 -2
- data/lib/identity_cache/cache_invalidation.rb +4 -11
- data/lib/identity_cache/cache_key_generation.rb +17 -65
- data/lib/identity_cache/cache_key_loader.rb +128 -0
- data/lib/identity_cache/cached.rb +7 -0
- data/lib/identity_cache/cached/association.rb +87 -0
- data/lib/identity_cache/cached/attribute.rb +123 -0
- data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
- data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
- data/lib/identity_cache/cached/belongs_to.rb +93 -0
- data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
- data/lib/identity_cache/cached/prefetcher.rb +51 -0
- data/lib/identity_cache/cached/primary_index.rb +97 -0
- data/lib/identity_cache/cached/recursive/association.rb +68 -0
- data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
- data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
- data/lib/identity_cache/cached/reference/association.rb +16 -0
- data/lib/identity_cache/cached/reference/has_many.rb +105 -0
- data/lib/identity_cache/cached/reference/has_one.rb +100 -0
- data/lib/identity_cache/configuration_dsl.rb +53 -215
- data/lib/identity_cache/encoder.rb +95 -0
- data/lib/identity_cache/expiry_hook.rb +36 -0
- data/lib/identity_cache/fallback_fetcher.rb +2 -1
- data/lib/identity_cache/load_strategy/eager.rb +28 -0
- data/lib/identity_cache/load_strategy/lazy.rb +71 -0
- data/lib/identity_cache/load_strategy/load_request.rb +20 -0
- data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
- data/lib/identity_cache/memoized_cache_proxy.rb +127 -58
- data/lib/identity_cache/parent_model_expiration.rb +45 -11
- data/lib/identity_cache/query_api.rb +128 -394
- data/lib/identity_cache/railtie.rb +8 -0
- data/lib/identity_cache/record_not_found.rb +6 -0
- data/lib/identity_cache/should_use_cache.rb +1 -0
- data/lib/identity_cache/version.rb +3 -2
- data/lib/identity_cache/with_primary_index.rb +136 -0
- data/lib/identity_cache/without_primary_index.rb +24 -3
- data/performance/cache_runner.rb +28 -34
- data/performance/cpu.rb +3 -2
- data/performance/externals.rb +4 -3
- data/performance/profile.rb +6 -5
- data/railgun.yml +16 -0
- metadata +44 -73
- data/Gemfile.rails42 +0 -6
- data/Gemfile.rails50 +0 -6
- data/test/attribute_cache_test.rb +0 -110
- data/test/cache_fetch_includes_test.rb +0 -46
- data/test/cache_hash_test.rb +0 -14
- data/test/cache_invalidation_test.rb +0 -139
- data/test/deeply_nested_associated_record_test.rb +0 -19
- data/test/denormalized_has_many_test.rb +0 -214
- data/test/denormalized_has_one_test.rb +0 -160
- data/test/fetch_multi_test.rb +0 -308
- data/test/fetch_test.rb +0 -258
- data/test/fixtures/serialized_record.mysql2 +0 -0
- data/test/fixtures/serialized_record.postgresql +0 -0
- data/test/helpers/active_record_objects.rb +0 -106
- data/test/helpers/database_connection.rb +0 -72
- data/test/helpers/serialization_format.rb +0 -51
- data/test/helpers/update_serialization_format.rb +0 -27
- data/test/identity_cache_test.rb +0 -29
- data/test/index_cache_test.rb +0 -161
- data/test/memoized_attributes_test.rb +0 -59
- data/test/memoized_cache_proxy_test.rb +0 -107
- data/test/normalized_belongs_to_test.rb +0 -107
- data/test/normalized_has_many_test.rb +0 -231
- data/test/normalized_has_one_test.rb +0 -9
- data/test/prefetch_associations_test.rb +0 -379
- data/test/readonly_test.rb +0 -109
- data/test/recursive_denormalized_has_many_test.rb +0 -131
- data/test/save_test.rb +0 -82
- data/test/schema_change_test.rb +0 -112
- data/test/serialization_format_change_test.rb +0 -16
- data/test/test_helper.rb +0 -140
data/lib/identity_cache.rb
CHANGED
@@ -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
|
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
|
-
|
54
|
-
|
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.
|
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 =
|
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
|
176
|
-
result.merge!
|
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
|
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
|
12
|
+
def cache_belongs_to(association)
|
12
13
|
ensure_base_model
|
13
|
-
raise NotImplementedError if options[:embed]
|
14
14
|
|
15
|
-
unless
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
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
|
-
|
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, :
|
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, :
|
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
|
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).
|
15
|
-
|
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}:"
|
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.
|
11
|
-
|
12
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|