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