identity_cache 0.5.1 → 1.0.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.
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