identity_cache 0.4.1 → 1.1.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 +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
|