identity_cache 0.4.1 → 1.1.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 +92 -0
- data/.gitignore +2 -0
- data/.rubocop.yml +5 -0
- data/CAVEATS.md +25 -0
- data/CHANGELOG.md +73 -19
- data/Gemfile +5 -1
- data/LICENSE +1 -1
- data/README.md +49 -27
- data/Rakefile +14 -5
- data/dev.yml +12 -16
- data/gemfiles/Gemfile.latest-release +8 -0
- data/gemfiles/Gemfile.min-supported +7 -0
- data/gemfiles/Gemfile.rails-edge +7 -0
- data/identity_cache.gemspec +29 -10
- data/lib/identity_cache.rb +78 -51
- 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 +100 -0
- data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
- data/lib/identity_cache/cached/prefetcher.rb +61 -0
- data/lib/identity_cache/cached/primary_index.rb +96 -0
- data/lib/identity_cache/cached/recursive/association.rb +109 -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/mem_cache_store_cas.rb +53 -0
- data/lib/identity_cache/memoized_cache_proxy.rb +137 -58
- data/lib/identity_cache/parent_model_expiration.rb +46 -11
- data/lib/identity_cache/query_api.rb +102 -408
- 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 +25 -73
- data/performance/cpu.rb +4 -3
- data/performance/externals.rb +4 -3
- data/performance/profile.rb +6 -5
- data/railgun.yml +16 -0
- metadata +60 -73
- data/.travis.yml +0 -30
- 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 -211
- 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 -42
- data/test/helpers/update_serialization_format.rb +0 -24
- data/test/identity_cache_test.rb +0 -29
- data/test/index_cache_test.rb +0 -161
- data/test/memoized_attributes_test.rb +0 -49
- 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 -364
- 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
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
module Encoder
|
4
|
+
DEHYDRATE_EVENT = "dehydration.identity_cache"
|
5
|
+
HYDRATE_EVENT = "hydration.identity_cache"
|
6
|
+
|
7
|
+
class << self
|
8
|
+
def encode(record)
|
9
|
+
return unless record
|
10
|
+
|
11
|
+
ActiveSupport::Notifications.instrument(DEHYDRATE_EVENT, class: record.class.name) do
|
12
|
+
coder_from_record(record, record.class)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def decode(coder, klass)
|
17
|
+
return unless coder
|
18
|
+
|
19
|
+
ActiveSupport::Notifications.instrument(HYDRATE_EVENT, class: klass.name) do
|
20
|
+
record_from_coder(coder, klass)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def coder_from_record(record, klass)
|
27
|
+
return unless record
|
28
|
+
|
29
|
+
coder = {}
|
30
|
+
coder[:attributes] = record.attributes_before_type_cast.dup
|
31
|
+
|
32
|
+
recursively_embedded_associations = klass.send(:recursively_embedded_associations)
|
33
|
+
id_embedded_has_manys = klass.cached_has_manys.select { |_, association| association.embedded_by_reference? }
|
34
|
+
id_embedded_has_ones = klass.cached_has_ones.select { |_, association| association.embedded_by_reference? }
|
35
|
+
|
36
|
+
if recursively_embedded_associations.present?
|
37
|
+
coder[:associations] = recursively_embedded_associations.each_with_object({}) do |(name, association), hash|
|
38
|
+
hash[name] = IdentityCache.map_cached_nil_for(embedded_coder(record, name, association))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
if id_embedded_has_manys.present?
|
43
|
+
coder[:association_ids] = id_embedded_has_manys.each_with_object({}) do |(name, association), hash|
|
44
|
+
hash[name] = record.instance_variable_get(association.ids_variable_name)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
if id_embedded_has_ones.present?
|
49
|
+
coder[:association_id] = id_embedded_has_ones.each_with_object({}) do |(name, association), hash|
|
50
|
+
hash[name] = record.instance_variable_get(association.id_variable_name)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
coder
|
55
|
+
end
|
56
|
+
|
57
|
+
def embedded_coder(record, _association, cached_association)
|
58
|
+
embedded_record_or_records = record.public_send(cached_association.cached_accessor_name)
|
59
|
+
|
60
|
+
if embedded_record_or_records.respond_to?(:to_ary)
|
61
|
+
embedded_record_or_records.map do |embedded_record|
|
62
|
+
coder_from_record(embedded_record, embedded_record.class)
|
63
|
+
end
|
64
|
+
else
|
65
|
+
coder_from_record(embedded_record_or_records, embedded_record_or_records.class)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def record_from_coder(coder, klass) #:nodoc:
|
70
|
+
record = klass.instantiate(coder[:attributes].dup)
|
71
|
+
|
72
|
+
if coder.key?(:associations)
|
73
|
+
coder[:associations].each do |name, value|
|
74
|
+
record.instance_variable_set(klass.cached_association(name).dehydrated_variable_name, value)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
if coder.key?(:association_ids)
|
78
|
+
coder[:association_ids].each do |name, ids|
|
79
|
+
record.instance_variable_set(klass.cached_has_manys.fetch(name).ids_variable_name, ids)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
if coder.key?(:association_id)
|
83
|
+
coder[:association_id].each do |name, id|
|
84
|
+
record.instance_variable_set(klass.cached_has_ones.fetch(name).id_variable_name, id)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
record.readonly! if IdentityCache.fetch_read_only_records
|
89
|
+
record
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
private_constant :Encoder
|
95
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
class ExpiryHook
|
4
|
+
def initialize(cached_association)
|
5
|
+
@cached_association = cached_association
|
6
|
+
end
|
7
|
+
|
8
|
+
def install
|
9
|
+
cached_association.validate
|
10
|
+
entry = [parent_class, only_on_foreign_key_change?]
|
11
|
+
child_class.parent_expiration_entries[inverse_name] << entry
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
attr_reader :cached_association
|
17
|
+
|
18
|
+
def only_on_foreign_key_change?
|
19
|
+
cached_association.embedded_by_reference? && !cached_association.reflection.has_scope?
|
20
|
+
end
|
21
|
+
|
22
|
+
def inverse_name
|
23
|
+
cached_association.inverse_name
|
24
|
+
end
|
25
|
+
|
26
|
+
def parent_class
|
27
|
+
cached_association.reflection.active_record
|
28
|
+
end
|
29
|
+
|
30
|
+
def child_class
|
31
|
+
cached_association.reflection.klass
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
private_constant :ExpiryHook
|
36
|
+
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
module IdentityCache
|
2
3
|
class FallbackFetcher
|
3
4
|
attr_accessor :cache_backend
|
@@ -18,7 +19,7 @@ module IdentityCache
|
|
18
19
|
@cache_backend.clear
|
19
20
|
end
|
20
21
|
|
21
|
-
def fetch_multi(keys
|
22
|
+
def fetch_multi(keys)
|
22
23
|
results = @cache_backend.read_multi(*keys)
|
23
24
|
missed_keys = keys - results.keys
|
24
25
|
unless missed_keys.empty?
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
module LoadStrategy
|
5
|
+
module Eager
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def load(cache_fetcher, db_key)
|
9
|
+
yield CacheKeyLoader.load(cache_fetcher, db_key)
|
10
|
+
end
|
11
|
+
|
12
|
+
def load_multi(cache_fetcher, db_keys)
|
13
|
+
yield CacheKeyLoader.load_multi(cache_fetcher, db_keys)
|
14
|
+
end
|
15
|
+
|
16
|
+
def load_batch(db_keys_by_cache_fetcher)
|
17
|
+
yield CacheKeyLoader.load_batch(db_keys_by_cache_fetcher)
|
18
|
+
end
|
19
|
+
|
20
|
+
def lazy_load
|
21
|
+
lazy_loader = Lazy.new
|
22
|
+
yield lazy_loader
|
23
|
+
lazy_loader.load_now
|
24
|
+
nil
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
module LoadStrategy
|
5
|
+
class Lazy
|
6
|
+
def initialize
|
7
|
+
@pending_loads = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def load(cache_fetcher, db_key)
|
11
|
+
load_multi(cache_fetcher, [db_key]) do |results|
|
12
|
+
yield results.fetch(db_key)
|
13
|
+
end
|
14
|
+
nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def load_multi(cache_fetcher, db_keys, &callback)
|
18
|
+
load_request = LoadRequest.new(db_keys, callback)
|
19
|
+
|
20
|
+
if (prev_load_request = @pending_loads[cache_fetcher])
|
21
|
+
if prev_load_request.instance_of?(MultiLoadRequest)
|
22
|
+
prev_load_request.load_requests << load_request
|
23
|
+
else
|
24
|
+
@pending_loads[cache_fetcher] = MultiLoadRequest.new([prev_load_request, load_request])
|
25
|
+
end
|
26
|
+
else
|
27
|
+
@pending_loads[cache_fetcher] = LoadRequest.new(db_keys, callback)
|
28
|
+
end
|
29
|
+
nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def load_batch(db_keys_by_cache_fetcher)
|
33
|
+
batch_result = {}
|
34
|
+
db_keys_by_cache_fetcher.each do |cache_fetcher, db_keys|
|
35
|
+
load_multi(cache_fetcher, db_keys) do |load_result|
|
36
|
+
batch_result[cache_fetcher] = load_result
|
37
|
+
if batch_result.size == db_keys_by_cache_fetcher.size
|
38
|
+
yield batch_result
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
nil
|
43
|
+
end
|
44
|
+
|
45
|
+
def lazy_load
|
46
|
+
yield self
|
47
|
+
nil
|
48
|
+
end
|
49
|
+
|
50
|
+
def load_now
|
51
|
+
until @pending_loads.empty?
|
52
|
+
pending_loads = @pending_loads
|
53
|
+
@pending_loads = {}
|
54
|
+
load_pending(pending_loads)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def load_pending(pending_loads)
|
61
|
+
result = CacheKeyLoader.load_batch(pending_loads.transform_values(&:db_keys))
|
62
|
+
result.each do |cache_fetcher, load_result|
|
63
|
+
load_request = pending_loads.fetch(cache_fetcher)
|
64
|
+
load_request.after_load(load_result)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
private_constant :Lazy
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
module LoadStrategy
|
5
|
+
class LoadRequest
|
6
|
+
attr_reader :db_keys
|
7
|
+
|
8
|
+
def initialize(db_keys, callback)
|
9
|
+
@db_keys = db_keys
|
10
|
+
@callback = callback
|
11
|
+
end
|
12
|
+
|
13
|
+
def after_load(results)
|
14
|
+
@callback.call(results)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private_constant :LoadRequest
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
module LoadStrategy
|
5
|
+
class MultiLoadRequest
|
6
|
+
def initialize(load_requests)
|
7
|
+
@load_requests = load_requests
|
8
|
+
end
|
9
|
+
|
10
|
+
def db_keys
|
11
|
+
@load_requests.flat_map(&:db_keys).tap(&:uniq!)
|
12
|
+
end
|
13
|
+
|
14
|
+
def after_load(all_results)
|
15
|
+
@load_requests.each do |load_request|
|
16
|
+
load_result = {}
|
17
|
+
load_request.db_keys.each do |key|
|
18
|
+
load_result[key] = all_results[key]
|
19
|
+
end
|
20
|
+
load_request.after_load(load_result)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
private_constant :MultiLoadRequest
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'dalli/cas/client'
|
3
|
+
|
4
|
+
module IdentityCache
|
5
|
+
module MemCacheStoreCAS
|
6
|
+
def cas(name, options = nil)
|
7
|
+
options = merged_options(options)
|
8
|
+
key = normalize_key(name, options)
|
9
|
+
|
10
|
+
rescue_error_with(false) do
|
11
|
+
instrument(:cas, key, options) do
|
12
|
+
@data.with do |connection|
|
13
|
+
connection.cas(key, options[:expires_in].to_i, options) do |raw_value|
|
14
|
+
entry = deserialize_entry(raw_value)
|
15
|
+
value = yield entry.value
|
16
|
+
entry = ActiveSupport::Cache::Entry.new(value, **options)
|
17
|
+
options[:raw] ? entry.value.to_s : entry
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def cas_multi(*names, **options)
|
25
|
+
return if names.empty?
|
26
|
+
|
27
|
+
options = merged_options(options)
|
28
|
+
keys_to_names = names.each_with_object({}) { |name, hash| hash[normalize_key(name, options)] = name }
|
29
|
+
keys = keys_to_names.keys
|
30
|
+
rescue_error_with(false) do
|
31
|
+
instrument(:cas_multi, keys, options) do
|
32
|
+
raw_values = @data.get_multi_cas(keys)
|
33
|
+
|
34
|
+
values = {}
|
35
|
+
raw_values.each do |key, raw_value|
|
36
|
+
entry = deserialize_entry(raw_value.first)
|
37
|
+
values[keys_to_names[key]] = entry.value unless entry.expired?
|
38
|
+
end
|
39
|
+
|
40
|
+
updates = yield values
|
41
|
+
|
42
|
+
updates.each do |name, value|
|
43
|
+
key = normalize_key(name, options)
|
44
|
+
cas_id = raw_values[key].last
|
45
|
+
entry = ActiveSupport::Cache::Entry.new(value, **options)
|
46
|
+
payload = options[:raw] ? entry.value.to_s : entry
|
47
|
+
@data.replace_cas(key, payload, cas_id, options[:expires_in].to_i, options)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -1,4 +1,6 @@
|
|
1
|
+
# frozen_string_literal: true
|
1
2
|
require 'monitor'
|
3
|
+
require 'benchmark'
|
2
4
|
|
3
5
|
module IdentityCache
|
4
6
|
class MemoizedCacheProxy
|
@@ -6,13 +8,30 @@ module IdentityCache
|
|
6
8
|
|
7
9
|
def initialize(cache_adaptor = nil)
|
8
10
|
self.cache_backend = cache_adaptor || Rails.cache
|
9
|
-
@key_value_maps = Hash.new {|h, k| h[k] = {} }
|
11
|
+
@key_value_maps = Hash.new { |h, k| h[k] = {} }
|
10
12
|
end
|
11
13
|
|
12
14
|
def cache_backend=(cache_adaptor)
|
15
|
+
if cache_adaptor.class.name == 'ActiveSupport::Cache::MemCacheStore'
|
16
|
+
if cache_adaptor.respond_to?(:cas) || cache_adaptor.respond_to?(:cas_multi)
|
17
|
+
unless cache_adaptor.is_a?(MemCacheStoreCAS)
|
18
|
+
raise "#{cache_adaptor} respond to :cas or :cas_multi, that's unexpected"
|
19
|
+
end
|
20
|
+
else
|
21
|
+
cache_adaptor.extend(MemCacheStoreCAS)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
13
25
|
if cache_adaptor.respond_to?(:cas) && cache_adaptor.respond_to?(:cas_multi)
|
14
26
|
@cache_fetcher = CacheFetcher.new(cache_adaptor)
|
15
27
|
else
|
28
|
+
case cache_adaptor
|
29
|
+
when ActiveSupport::Cache::MemoryStore, ActiveSupport::Cache::NullStore
|
30
|
+
# no need for CAS support
|
31
|
+
else
|
32
|
+
warn("[IdentityCache] Missing CAS support in cache backend #{cache_adaptor.class} "\
|
33
|
+
"which is needed for cache consistency")
|
34
|
+
end
|
16
35
|
@cache_fetcher = FallbackFetcher.new(cache_adaptor)
|
17
36
|
end
|
18
37
|
end
|
@@ -21,7 +40,7 @@ module IdentityCache
|
|
21
40
|
@key_value_maps[Thread.current]
|
22
41
|
end
|
23
42
|
|
24
|
-
def with_memoization
|
43
|
+
def with_memoization
|
25
44
|
Thread.current[:memoizing_idc] = true
|
26
45
|
yield
|
27
46
|
ensure
|
@@ -30,101 +49,161 @@ module IdentityCache
|
|
30
49
|
end
|
31
50
|
|
32
51
|
def write(key, value)
|
33
|
-
|
34
|
-
|
52
|
+
memoizing = memoizing?
|
53
|
+
ActiveSupport::Notifications.instrument('cache_write.identity_cache', memoizing: memoizing) do
|
54
|
+
memoized_key_values[key] = value if memoizing
|
55
|
+
@cache_fetcher.write(key, value)
|
56
|
+
end
|
35
57
|
end
|
36
58
|
|
37
59
|
def delete(key)
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
60
|
+
memoizing = memoizing?
|
61
|
+
ActiveSupport::Notifications.instrument('cache_delete.identity_cache', memoizing: memoizing) do
|
62
|
+
memoized_key_values.delete(key) if memoizing
|
63
|
+
if (result = @cache_fetcher.delete(key))
|
64
|
+
IdentityCache.logger.debug { "[IdentityCache] delete recorded for #{key}" }
|
65
|
+
else
|
66
|
+
IdentityCache.logger.error { "[IdentityCache] delete failed for #{key}" }
|
67
|
+
end
|
68
|
+
result
|
69
|
+
end
|
42
70
|
end
|
43
71
|
|
44
72
|
def fetch(key)
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
73
|
+
memo_misses = 0
|
74
|
+
cache_misses = 0
|
75
|
+
|
76
|
+
value = ActiveSupport::Notifications.instrument('cache_fetch.identity_cache') do |payload|
|
77
|
+
payload[:resolve_miss_time] = 0.0
|
78
|
+
|
79
|
+
value = fetch_memoized(key) do
|
80
|
+
memo_misses = 1
|
81
|
+
@cache_fetcher.fetch(key) do
|
82
|
+
cache_misses = 1
|
83
|
+
instrument_duration(payload, :resolve_miss_time) do
|
84
|
+
yield
|
85
|
+
end
|
54
86
|
end
|
55
87
|
end
|
56
|
-
|
57
|
-
|
58
|
-
missed = true
|
59
|
-
yield
|
60
|
-
end
|
88
|
+
set_instrumentation_payload(payload, num_keys: 1, memo_misses: memo_misses, cache_misses: cache_misses)
|
89
|
+
value
|
61
90
|
end
|
62
91
|
|
63
|
-
if
|
92
|
+
if cache_misses > 0
|
64
93
|
IdentityCache.logger.debug { "[IdentityCache] cache miss for #{key}" }
|
65
94
|
else
|
66
|
-
IdentityCache.logger.debug
|
95
|
+
IdentityCache.logger.debug do
|
96
|
+
"[IdentityCache] #{memo_misses > 0 ? '(cache_backend)' : '(memoized)'} cache hit for #{key}"
|
97
|
+
end
|
67
98
|
end
|
68
99
|
|
69
100
|
value
|
70
101
|
end
|
71
102
|
|
72
103
|
def fetch_multi(*keys)
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
104
|
+
memo_miss_keys = EMPTY_ARRAY
|
105
|
+
cache_miss_keys = EMPTY_ARRAY
|
106
|
+
|
107
|
+
result = ActiveSupport::Notifications.instrument('cache_fetch_multi.identity_cache') do |payload|
|
108
|
+
payload[:resolve_miss_time] = 0.0
|
109
|
+
|
110
|
+
result = fetch_multi_memoized(keys) do |non_memoized_keys|
|
111
|
+
memo_miss_keys = non_memoized_keys
|
112
|
+
@cache_fetcher.fetch_multi(non_memoized_keys) do |missing_keys|
|
113
|
+
cache_miss_keys = missing_keys
|
114
|
+
instrument_duration(payload, :resolve_miss_time) do
|
115
|
+
yield missing_keys
|
116
|
+
end
|
85
117
|
end
|
86
118
|
end
|
87
119
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
end
|
96
|
-
hash
|
97
|
-
else
|
98
|
-
@cache_fetcher.fetch_multi(keys) do |missing_keys|
|
99
|
-
missed_keys.concat(missing_keys) if IdentityCache.logger.debug?
|
100
|
-
yield missing_keys
|
101
|
-
end
|
120
|
+
set_instrumentation_payload(
|
121
|
+
payload,
|
122
|
+
num_keys: keys.length,
|
123
|
+
memo_misses: memo_miss_keys.length,
|
124
|
+
cache_misses: cache_miss_keys.length
|
125
|
+
)
|
126
|
+
result
|
102
127
|
end
|
103
128
|
|
104
|
-
log_multi_result(
|
129
|
+
log_multi_result(keys, memo_miss_keys, cache_miss_keys)
|
105
130
|
|
106
131
|
result
|
107
132
|
end
|
108
133
|
|
109
134
|
def clear
|
110
|
-
|
111
|
-
|
135
|
+
ActiveSupport::Notifications.instrument('cache_clear.identity_cache') do
|
136
|
+
clear_memoization
|
137
|
+
@cache_fetcher.clear
|
138
|
+
end
|
112
139
|
end
|
113
140
|
|
114
141
|
private
|
115
142
|
|
143
|
+
EMPTY_ARRAY = [].freeze
|
144
|
+
private_constant :EMPTY_ARRAY
|
145
|
+
|
146
|
+
def set_instrumentation_payload(payload, num_keys:, memo_misses:, cache_misses:)
|
147
|
+
payload[:memoizing] = memoizing?
|
148
|
+
payload[:memo_hits] = num_keys - memo_misses
|
149
|
+
payload[:cache_hits] = memo_misses - cache_misses
|
150
|
+
payload[:cache_misses] = cache_misses
|
151
|
+
end
|
152
|
+
|
153
|
+
def fetch_memoized(key)
|
154
|
+
return yield unless memoizing?
|
155
|
+
if memoized_key_values.key?(key)
|
156
|
+
return memoized_key_values[key]
|
157
|
+
end
|
158
|
+
memoized_key_values[key] = yield
|
159
|
+
end
|
160
|
+
|
161
|
+
def fetch_multi_memoized(keys)
|
162
|
+
return yield keys unless memoizing?
|
163
|
+
|
164
|
+
result = {}
|
165
|
+
missing_keys = keys.reject do |key|
|
166
|
+
if memoized_key_values.key?(key)
|
167
|
+
result[key] = memoized_key_values[key]
|
168
|
+
true
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
unless missing_keys.empty?
|
173
|
+
block_result = yield missing_keys
|
174
|
+
memoized_key_values.merge!(block_result)
|
175
|
+
result.merge!(block_result)
|
176
|
+
end
|
177
|
+
|
178
|
+
result
|
179
|
+
end
|
180
|
+
|
181
|
+
def instrument_duration(payload, key)
|
182
|
+
value = nil
|
183
|
+
payload[key] += Benchmark.realtime do
|
184
|
+
value = yield
|
185
|
+
end
|
186
|
+
value
|
187
|
+
end
|
188
|
+
|
116
189
|
def clear_memoization
|
117
190
|
@key_value_maps.delete(Thread.current)
|
118
191
|
end
|
119
192
|
|
120
193
|
def memoizing?
|
121
|
-
Thread.current[:memoizing_idc]
|
194
|
+
!!Thread.current[:memoizing_idc]
|
122
195
|
end
|
123
196
|
|
124
|
-
def log_multi_result(
|
125
|
-
|
126
|
-
|
127
|
-
|
197
|
+
def log_multi_result(keys, memo_miss_keys, cache_miss_keys)
|
198
|
+
return unless IdentityCache.logger.debug?
|
199
|
+
|
200
|
+
memoized_keys = keys - memo_miss_keys
|
201
|
+
cache_hit_keys = memo_miss_keys - cache_miss_keys
|
202
|
+
missed_keys = cache_miss_keys
|
203
|
+
|
204
|
+
memoized_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (memoized) cache hit for #{k} (multi)") }
|
205
|
+
cache_hit_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (backend) cache hit for #{k} (multi)") }
|
206
|
+
missed_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] cache miss for #{k} (multi)") }
|
128
207
|
end
|
129
208
|
end
|
130
209
|
end
|