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.
Files changed (91) hide show
  1. checksums.yaml +5 -5
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/ci.yml +26 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +5 -0
  6. data/.travis.yml +24 -9
  7. data/CHANGELOG.md +21 -0
  8. data/Gemfile +5 -1
  9. data/README.md +28 -26
  10. data/Rakefile +14 -5
  11. data/dev.yml +9 -16
  12. data/gemfiles/Gemfile.latest-release +6 -0
  13. data/gemfiles/Gemfile.rails-edge +6 -0
  14. data/gemfiles/Gemfile.rails52 +6 -0
  15. data/identity_cache.gemspec +26 -10
  16. data/lib/identity_cache.rb +49 -46
  17. data/lib/identity_cache/belongs_to_caching.rb +12 -40
  18. data/lib/identity_cache/cache_fetcher.rb +6 -5
  19. data/lib/identity_cache/cache_hash.rb +2 -2
  20. data/lib/identity_cache/cache_invalidation.rb +4 -11
  21. data/lib/identity_cache/cache_key_generation.rb +17 -65
  22. data/lib/identity_cache/cache_key_loader.rb +128 -0
  23. data/lib/identity_cache/cached.rb +7 -0
  24. data/lib/identity_cache/cached/association.rb +87 -0
  25. data/lib/identity_cache/cached/attribute.rb +123 -0
  26. data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
  27. data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
  28. data/lib/identity_cache/cached/belongs_to.rb +93 -0
  29. data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
  30. data/lib/identity_cache/cached/prefetcher.rb +51 -0
  31. data/lib/identity_cache/cached/primary_index.rb +97 -0
  32. data/lib/identity_cache/cached/recursive/association.rb +68 -0
  33. data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
  34. data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
  35. data/lib/identity_cache/cached/reference/association.rb +16 -0
  36. data/lib/identity_cache/cached/reference/has_many.rb +105 -0
  37. data/lib/identity_cache/cached/reference/has_one.rb +100 -0
  38. data/lib/identity_cache/configuration_dsl.rb +53 -215
  39. data/lib/identity_cache/encoder.rb +95 -0
  40. data/lib/identity_cache/expiry_hook.rb +36 -0
  41. data/lib/identity_cache/fallback_fetcher.rb +2 -1
  42. data/lib/identity_cache/load_strategy/eager.rb +28 -0
  43. data/lib/identity_cache/load_strategy/lazy.rb +71 -0
  44. data/lib/identity_cache/load_strategy/load_request.rb +20 -0
  45. data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
  46. data/lib/identity_cache/memoized_cache_proxy.rb +127 -58
  47. data/lib/identity_cache/parent_model_expiration.rb +45 -11
  48. data/lib/identity_cache/query_api.rb +128 -394
  49. data/lib/identity_cache/railtie.rb +8 -0
  50. data/lib/identity_cache/record_not_found.rb +6 -0
  51. data/lib/identity_cache/should_use_cache.rb +1 -0
  52. data/lib/identity_cache/version.rb +3 -2
  53. data/lib/identity_cache/with_primary_index.rb +136 -0
  54. data/lib/identity_cache/without_primary_index.rb +24 -3
  55. data/performance/cache_runner.rb +28 -34
  56. data/performance/cpu.rb +3 -2
  57. data/performance/externals.rb +4 -3
  58. data/performance/profile.rb +6 -5
  59. data/railgun.yml +16 -0
  60. metadata +44 -73
  61. data/Gemfile.rails42 +0 -6
  62. data/Gemfile.rails50 +0 -6
  63. data/test/attribute_cache_test.rb +0 -110
  64. data/test/cache_fetch_includes_test.rb +0 -46
  65. data/test/cache_hash_test.rb +0 -14
  66. data/test/cache_invalidation_test.rb +0 -139
  67. data/test/deeply_nested_associated_record_test.rb +0 -19
  68. data/test/denormalized_has_many_test.rb +0 -214
  69. data/test/denormalized_has_one_test.rb +0 -160
  70. data/test/fetch_multi_test.rb +0 -308
  71. data/test/fetch_test.rb +0 -258
  72. data/test/fixtures/serialized_record.mysql2 +0 -0
  73. data/test/fixtures/serialized_record.postgresql +0 -0
  74. data/test/helpers/active_record_objects.rb +0 -106
  75. data/test/helpers/database_connection.rb +0 -72
  76. data/test/helpers/serialization_format.rb +0 -51
  77. data/test/helpers/update_serialization_format.rb +0 -27
  78. data/test/identity_cache_test.rb +0 -29
  79. data/test/index_cache_test.rb +0 -161
  80. data/test/memoized_attributes_test.rb +0 -59
  81. data/test/memoized_cache_proxy_test.rb +0 -107
  82. data/test/normalized_belongs_to_test.rb +0 -107
  83. data/test/normalized_has_many_test.rb +0 -231
  84. data/test/normalized_has_one_test.rb +0 -9
  85. data/test/prefetch_associations_test.rb +0 -379
  86. data/test/readonly_test.rb +0 -109
  87. data/test/recursive_denormalized_has_many_test.rb +0 -131
  88. data/test/save_test.rb +0 -82
  89. data/test/schema_change_test.rb +0 -112
  90. data/test/serialization_format_change_test.rb +0 -16
  91. 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,9 @@
1
+ # frozen_string_literal: true
2
+ module IdentityCache
3
+ module Cached
4
+ module Recursive
5
+ class HasMany < Association # :nodoc:
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ module IdentityCache
3
+ module Cached
4
+ module Recursive
5
+ class HasOne < Association # :nodoc:
6
+ end
7
+ end
8
+ end
9
+ 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