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
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
module Cached
|
5
|
+
module Prefetcher
|
6
|
+
ASSOCIATION_FETCH_EVENT = "association_fetch.identity_cache"
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def prefetch(klass, associations, records, load_strategy: LoadStrategy::Eager)
|
10
|
+
return if (records = records.to_a).empty?
|
11
|
+
|
12
|
+
case associations
|
13
|
+
when Symbol
|
14
|
+
fetch_association(load_strategy, klass, associations, records) {}
|
15
|
+
when Array
|
16
|
+
load_strategy.lazy_load do |lazy_loader|
|
17
|
+
associations.each do |association|
|
18
|
+
prefetch(klass, association, records, load_strategy: lazy_loader)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
when Hash
|
22
|
+
load_strategy.lazy_load do |lazy_loader|
|
23
|
+
associations.each do |association, sub_associations|
|
24
|
+
fetch_association(lazy_loader, klass, association, records) do |next_level_records|
|
25
|
+
if sub_associations.present?
|
26
|
+
association_class = klass.reflect_on_association(association).klass
|
27
|
+
prefetch(association_class, sub_associations, next_level_records, load_strategy: lazy_loader)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
else
|
33
|
+
raise TypeError, "Invalid associations class #{associations.class}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def fetch_association(load_strategy, klass, association, records, &block)
|
40
|
+
unless records.first.class.should_use_cache?
|
41
|
+
ActiveRecord::Associations::Preloader.new.preload(records, association)
|
42
|
+
return yield
|
43
|
+
end
|
44
|
+
|
45
|
+
cached_association = klass.cached_association(association)
|
46
|
+
cached_association.fetch_async(load_strategy, records, &block)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module IdentityCache
|
4
|
+
module Cached
|
5
|
+
class PrimaryIndex
|
6
|
+
attr_reader :model
|
7
|
+
|
8
|
+
def initialize(model)
|
9
|
+
@model = model
|
10
|
+
end
|
11
|
+
|
12
|
+
def fetch(id)
|
13
|
+
id = cast_id(id)
|
14
|
+
return unless id
|
15
|
+
record = if model.should_use_cache?
|
16
|
+
object = CacheKeyLoader.load(self, id)
|
17
|
+
if object && object.id != id
|
18
|
+
IdentityCache.logger.error(
|
19
|
+
<<~MSG.squish
|
20
|
+
[IDC id mismatch] fetch_by_id_requested=#{id}
|
21
|
+
fetch_by_id_got=#{object.id}
|
22
|
+
for #{object.inspect[(0..100)]}
|
23
|
+
MSG
|
24
|
+
)
|
25
|
+
end
|
26
|
+
object
|
27
|
+
else
|
28
|
+
load_one_from_db(id)
|
29
|
+
end
|
30
|
+
record
|
31
|
+
end
|
32
|
+
|
33
|
+
def fetch_multi(ids)
|
34
|
+
ids.map! { |id| cast_id(id) }.compact!
|
35
|
+
id_to_record_hash = if model.should_use_cache?
|
36
|
+
id_to_record_hash = CacheKeyLoader.load_multi(self, ids)
|
37
|
+
else
|
38
|
+
load_multi_from_db(ids)
|
39
|
+
end
|
40
|
+
records = ids.map { |id| id_to_record_hash[id] }
|
41
|
+
records.compact!
|
42
|
+
records
|
43
|
+
end
|
44
|
+
|
45
|
+
def expire(id)
|
46
|
+
id = cast_id(id)
|
47
|
+
IdentityCache.cache.delete(cache_key(id))
|
48
|
+
end
|
49
|
+
|
50
|
+
def cache_key(id)
|
51
|
+
"#{model.rails_cache_key_namespace}#{cache_key_prefix}#{id}"
|
52
|
+
end
|
53
|
+
|
54
|
+
def load_one_from_db(id)
|
55
|
+
record = build_query(id).take
|
56
|
+
model.send(:setup_embedded_associations_on_miss, [record]) if record
|
57
|
+
record
|
58
|
+
end
|
59
|
+
|
60
|
+
def load_multi_from_db(ids)
|
61
|
+
return {} if ids.empty?
|
62
|
+
|
63
|
+
ids = ids.map { |id| model.connection.type_cast(id, id_column) }
|
64
|
+
records = build_query(ids).to_a
|
65
|
+
model.send(:setup_embedded_associations_on_miss, records)
|
66
|
+
records.index_by(&:id)
|
67
|
+
end
|
68
|
+
|
69
|
+
def cache_encode(record)
|
70
|
+
Encoder.encode(record)
|
71
|
+
end
|
72
|
+
|
73
|
+
def cache_decode(cache_value)
|
74
|
+
Encoder.decode(cache_value, model)
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def cast_id(id)
|
80
|
+
model.type_for_attribute(model.primary_key).cast(id)
|
81
|
+
end
|
82
|
+
|
83
|
+
def id_column
|
84
|
+
@id_column ||= model.columns.detect { |c| c.name == model.primary_key }
|
85
|
+
end
|
86
|
+
|
87
|
+
def build_query(id_or_ids)
|
88
|
+
model.where(model.primary_key => id_or_ids).includes(model.send(:cache_fetch_includes))
|
89
|
+
end
|
90
|
+
|
91
|
+
def cache_key_prefix
|
92
|
+
@cache_key_prefix ||= "blob:#{model.base_class.name}:" \
|
93
|
+
"#{IdentityCache::CacheKeyGeneration.denormalized_schema_hash(model)}:"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
module Cached
|
4
|
+
module Recursive
|
5
|
+
class Association < Cached::Association # :nodoc:
|
6
|
+
def initialize(name, reflection:)
|
7
|
+
super
|
8
|
+
@dehydrated_variable_name = :"@dehydrated_#{name}"
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :dehydrated_variable_name
|
12
|
+
|
13
|
+
def build
|
14
|
+
reflection.active_record.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
15
|
+
def #{cached_accessor_name}
|
16
|
+
fetch_recursively_cached_association(
|
17
|
+
:#{records_variable_name},
|
18
|
+
:#{dehydrated_variable_name},
|
19
|
+
:#{name}
|
20
|
+
)
|
21
|
+
end
|
22
|
+
RUBY
|
23
|
+
|
24
|
+
ParentModelExpiration.add_parent_expiry_hook(self)
|
25
|
+
end
|
26
|
+
|
27
|
+
def read(record)
|
28
|
+
record.public_send(cached_accessor_name)
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(record, records)
|
32
|
+
record.instance_variable_set(records_variable_name, records)
|
33
|
+
end
|
34
|
+
|
35
|
+
def clear(record)
|
36
|
+
if record.instance_variable_defined?(records_variable_name)
|
37
|
+
record.remove_instance_variable(records_variable_name)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def fetch(records)
|
42
|
+
fetch_async(LoadStrategy::Eager, records) { |child_records| child_records }
|
43
|
+
end
|
44
|
+
|
45
|
+
def fetch_async(load_strategy, records)
|
46
|
+
fetch_embedded_async(load_strategy, records) do
|
47
|
+
yield records.flat_map(&cached_accessor_name).tap(&:compact!)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def embedded_by_reference?
|
52
|
+
false
|
53
|
+
end
|
54
|
+
|
55
|
+
def embedded_recursively?
|
56
|
+
true
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def embedded_fetched?(records)
|
62
|
+
record = records.first
|
63
|
+
super || record.instance_variable_defined?(dehydrated_variable_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
module Cached
|
4
|
+
module Reference
|
5
|
+
class Association < Cached::Association # :nodoc:
|
6
|
+
def embedded_by_reference?
|
7
|
+
true
|
8
|
+
end
|
9
|
+
|
10
|
+
def embedded_recursively?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
module Cached
|
4
|
+
module Reference
|
5
|
+
class HasMany < Association # :nodoc:
|
6
|
+
def initialize(name, reflection:)
|
7
|
+
super
|
8
|
+
@cached_ids_name = "fetch_#{ids_name}"
|
9
|
+
@ids_variable_name = :"@#{ids_cached_reader_name}"
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :cached_ids_name, :ids_variable_name
|
13
|
+
|
14
|
+
def build
|
15
|
+
reflection.active_record.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
16
|
+
attr_reader :#{ids_cached_reader_name}
|
17
|
+
|
18
|
+
def #{cached_ids_name}
|
19
|
+
#{ids_variable_name} ||= #{ids_name}
|
20
|
+
end
|
21
|
+
|
22
|
+
def #{cached_accessor_name}
|
23
|
+
association_klass = association(:#{name}).klass
|
24
|
+
if association_klass.should_use_cache? && !#{name}.loaded?
|
25
|
+
#{records_variable_name} ||= #{reflection.class_name}.fetch_multi(#{cached_ids_name})
|
26
|
+
else
|
27
|
+
#{name}.to_a
|
28
|
+
end
|
29
|
+
end
|
30
|
+
RUBY
|
31
|
+
|
32
|
+
ParentModelExpiration.add_parent_expiry_hook(self)
|
33
|
+
end
|
34
|
+
|
35
|
+
def read(record)
|
36
|
+
record.public_send(cached_ids_name)
|
37
|
+
end
|
38
|
+
|
39
|
+
def write(record, ids)
|
40
|
+
record.instance_variable_set(ids_variable_name, ids)
|
41
|
+
end
|
42
|
+
|
43
|
+
def clear(record)
|
44
|
+
[ids_variable_name, records_variable_name].each do |ivar|
|
45
|
+
if record.instance_variable_defined?(ivar)
|
46
|
+
record.remove_instance_variable(ivar)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def fetch(records)
|
52
|
+
fetch_async(LoadStrategy::Eager, records) { |child_records| child_records }
|
53
|
+
end
|
54
|
+
|
55
|
+
def fetch_async(load_strategy, records)
|
56
|
+
fetch_embedded_async(load_strategy, records) do
|
57
|
+
ids_to_parent_record = records.each_with_object({}) do |record, hash|
|
58
|
+
child_ids = record.send(cached_ids_name)
|
59
|
+
child_ids.each do |child_id|
|
60
|
+
hash[child_id] = record
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
load_strategy.load_multi(
|
65
|
+
reflection.klass.cached_primary_index,
|
66
|
+
ids_to_parent_record.keys
|
67
|
+
) do |child_records_by_id|
|
68
|
+
parent_record_to_child_records = Hash.new { |h, k| h[k] = [] }
|
69
|
+
|
70
|
+
child_records_by_id.each do |id, child_record|
|
71
|
+
parent_record = ids_to_parent_record.fetch(id)
|
72
|
+
parent_record_to_child_records[parent_record] << child_record
|
73
|
+
end
|
74
|
+
|
75
|
+
parent_record_to_child_records.each do |parent, children|
|
76
|
+
parent.instance_variable_set(records_variable_name, children)
|
77
|
+
end
|
78
|
+
|
79
|
+
yield child_records_by_id.values.compact
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
private
|
85
|
+
|
86
|
+
def embedded_fetched?(records)
|
87
|
+
record = records.first
|
88
|
+
super || record.instance_variable_defined?(ids_variable_name)
|
89
|
+
end
|
90
|
+
|
91
|
+
def singular_name
|
92
|
+
name.to_s.singularize
|
93
|
+
end
|
94
|
+
|
95
|
+
def ids_name
|
96
|
+
"#{singular_name}_ids"
|
97
|
+
end
|
98
|
+
|
99
|
+
def ids_cached_reader_name
|
100
|
+
"cached_#{ids_name}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module IdentityCache
|
3
|
+
module Cached
|
4
|
+
module Reference
|
5
|
+
class HasOne < Association # :nodoc:
|
6
|
+
def initialize(name, reflection:)
|
7
|
+
super
|
8
|
+
@cached_id_name = "fetch_#{id_name}"
|
9
|
+
@id_variable_name = :"@#{id_cached_reader_name}"
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :cached_id_name, :id_variable_name
|
13
|
+
|
14
|
+
def build
|
15
|
+
reflection.active_record.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
|
16
|
+
attr_reader :#{id_cached_reader_name}
|
17
|
+
|
18
|
+
def #{cached_id_name}
|
19
|
+
return #{id_variable_name} if defined?(#{id_variable_name})
|
20
|
+
#{id_variable_name} = association(:#{name}).scope.ids.first
|
21
|
+
end
|
22
|
+
|
23
|
+
def #{cached_accessor_name}
|
24
|
+
association_klass = association(:#{name}).klass
|
25
|
+
if association_klass.should_use_cache? && !association(:#{name}).loaded?
|
26
|
+
#{records_variable_name} ||= #{reflection.class_name}.fetch(#{cached_id_name}) if #{cached_id_name}
|
27
|
+
else
|
28
|
+
#{name}
|
29
|
+
end
|
30
|
+
end
|
31
|
+
RUBY
|
32
|
+
|
33
|
+
ParentModelExpiration.add_parent_expiry_hook(self)
|
34
|
+
end
|
35
|
+
|
36
|
+
def read(record)
|
37
|
+
record.public_send(cached_id_name)
|
38
|
+
end
|
39
|
+
|
40
|
+
def write(record, id)
|
41
|
+
record.instance_variable_set(id_variable_name, id)
|
42
|
+
end
|
43
|
+
|
44
|
+
def clear(record)
|
45
|
+
[id_variable_name, records_variable_name].each do |ivar|
|
46
|
+
if record.instance_variable_defined?(ivar)
|
47
|
+
record.remove_instance_variable(ivar)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def fetch(records)
|
53
|
+
fetch_async(LoadStrategy::Eager, records) { |child_records| child_records }
|
54
|
+
end
|
55
|
+
|
56
|
+
def fetch_async(load_strategy, records)
|
57
|
+
fetch_embedded_async(load_strategy, records) do
|
58
|
+
ids_to_parent_record = records.each_with_object({}) do |record, hash|
|
59
|
+
child_id = record.send(cached_id_name)
|
60
|
+
hash[child_id] = record if child_id
|
61
|
+
end
|
62
|
+
|
63
|
+
load_strategy.load_multi(
|
64
|
+
reflection.klass.cached_primary_index,
|
65
|
+
ids_to_parent_record.keys
|
66
|
+
) do |child_records_by_id|
|
67
|
+
parent_record_to_child_record = {}
|
68
|
+
|
69
|
+
child_records_by_id.each do |id, child_record|
|
70
|
+
parent_record = ids_to_parent_record.fetch(id)
|
71
|
+
parent_record_to_child_record[parent_record] ||= child_record
|
72
|
+
end
|
73
|
+
|
74
|
+
parent_record_to_child_record.each do |parent, child|
|
75
|
+
parent.instance_variable_set(records_variable_name, child)
|
76
|
+
end
|
77
|
+
|
78
|
+
yield child_records_by_id.values.compact
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
def embedded_fetched?(records)
|
86
|
+
record = records.first
|
87
|
+
super || record.instance_variable_defined?(id_variable_name)
|
88
|
+
end
|
89
|
+
|
90
|
+
def id_name
|
91
|
+
"#{name}_id"
|
92
|
+
end
|
93
|
+
|
94
|
+
def id_cached_reader_name
|
95
|
+
"cached_#{id_name}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|