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,7 @@
1
+ # frozen_string_literal: true
2
+ module IdentityCache
3
+ module Cached
4
+ end
5
+
6
+ private_constant :Cached
7
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+ module IdentityCache
3
+ module Cached
4
+ class Association # :nodoc:
5
+ include EmbeddedFetching
6
+
7
+ def initialize(name, reflection:)
8
+ @name = name
9
+ @reflection = reflection
10
+ @cached_accessor_name = :"fetch_#{name}"
11
+ @records_variable_name = :"@cached_#{name}"
12
+ end
13
+
14
+ attr_reader :name, :reflection, :cached_accessor_name, :records_variable_name
15
+
16
+ def build
17
+ raise NotImplementedError
18
+ end
19
+
20
+ def read(_record)
21
+ raise NotImplementedError
22
+ end
23
+
24
+ def write(_record, _value)
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def clear(_record)
29
+ raise NotImplementedError
30
+ end
31
+
32
+ def fetch(_records)
33
+ raise NotImplementedError
34
+ end
35
+
36
+ def fetch_async(_load_strategy, _records)
37
+ raise NotImplementedError
38
+ end
39
+
40
+ def embedded?
41
+ embedded_by_reference? || embedded_recursively?
42
+ end
43
+
44
+ def embedded_by_reference?
45
+ raise NotImplementedError
46
+ end
47
+
48
+ def embedded_recursively?
49
+ raise NotImplementedError
50
+ end
51
+
52
+ def inverse_name
53
+ @inverse_name ||= begin
54
+ reflection.inverse_of&.name ||
55
+ reflection.active_record.name.underscore
56
+ end
57
+ end
58
+
59
+ def validate
60
+ parent_class = reflection.active_record
61
+ child_class = reflection.klass
62
+
63
+ unless child_class < IdentityCache::WithoutPrimaryIndex
64
+ if embedded_recursively?
65
+ raise UnsupportedAssociationError, <<~MSG.squish
66
+ cached association #{parent_class}\##{reflection.name} requires
67
+ associated class #{child_class} to include IdentityCache
68
+ or IdentityCache::WithoutPrimaryIndex
69
+ MSG
70
+ else
71
+ raise UnsupportedAssociationError, <<~MSG.squish
72
+ cached association #{parent_class}\##{reflection.name} requires
73
+ associated class #{child_class} to include IdentityCache
74
+ MSG
75
+ end
76
+ end
77
+
78
+ unless child_class.reflect_on_association(inverse_name)
79
+ raise InverseAssociationError, <<~MSG
80
+ Inverse name for association #{parent_class}\##{reflection.name} could not be determined.
81
+ Use the :inverse_of option on the Active Record association to specify the inverse association name.
82
+ MSG
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IdentityCache
4
+ module Cached
5
+ # @abstract
6
+ class Attribute
7
+ attr_reader :model, :alias_name, :key_fields, :unique
8
+
9
+ def initialize(model, attribute_or_proc, alias_name, key_fields, unique)
10
+ @model = model
11
+ if attribute_or_proc.is_a?(Proc)
12
+ @attribute_proc = attribute_or_proc
13
+ else
14
+ @attribute = attribute_or_proc.to_sym
15
+ end
16
+ @alias_name = alias_name.to_sym
17
+ @key_fields = key_fields.map(&:to_sym)
18
+ @unique = !!unique
19
+ end
20
+
21
+ def attribute
22
+ @attribute ||= @attribute_proc.call.to_sym
23
+ end
24
+
25
+ def fetch(db_key)
26
+ db_key = cast_db_key(db_key)
27
+
28
+ if model.should_use_cache?
29
+ IdentityCache.fetch(cache_key(db_key)) do
30
+ load_one_from_db(db_key)
31
+ end
32
+ else
33
+ load_one_from_db(db_key)
34
+ end
35
+ end
36
+
37
+ def expire(record)
38
+ unless record.send(:was_new_record?)
39
+ old_key = old_cache_key(record)
40
+ IdentityCache.cache.delete(old_key)
41
+ end
42
+ unless record.destroyed?
43
+ new_key = new_cache_key(record)
44
+ if new_key != old_key
45
+ IdentityCache.cache.delete(new_key)
46
+ end
47
+ end
48
+ end
49
+
50
+ def cache_key(index_key)
51
+ values_hash = IdentityCache.memcache_hash(unhashed_values_cache_key_string(index_key))
52
+ "#{model.rails_cache_key_namespace}#{cache_key_prefix}#{values_hash}"
53
+ end
54
+
55
+ def load_one_from_db(key)
56
+ query = model.reorder(nil).where(load_from_db_where_conditions(key))
57
+ query = query.limit(1) if unique
58
+ results = query.pluck(attribute)
59
+ unique ? results.first : results
60
+ end
61
+
62
+ private
63
+
64
+ # @abstract
65
+ def cast_db_key(_index_key)
66
+ raise NotImplementedError
67
+ end
68
+
69
+ # @abstract
70
+ def unhashed_values_cache_key_string(_index_key)
71
+ raise NotImplementedError
72
+ end
73
+
74
+ # @abstract
75
+ def load_from_db_where_conditions(_index_key_or_keys)
76
+ raise NotImplementedError
77
+ end
78
+
79
+ # @abstract
80
+ def cache_key_from_key_values(_key_values)
81
+ raise NotImplementedError
82
+ end
83
+
84
+ def field_types
85
+ @field_types ||= key_fields.map { |field| model.type_for_attribute(field.to_s) }
86
+ end
87
+
88
+ def cache_key_prefix
89
+ @cache_key_prefix ||= begin
90
+ unique_indicator = unique ? '' : 's'
91
+ "attr#{unique_indicator}" \
92
+ ":#{model.base_class.name}" \
93
+ ":#{attribute}" \
94
+ ":#{key_fields.join('/')}:"
95
+ end
96
+ end
97
+
98
+ def new_cache_key(record)
99
+ new_key_values = key_fields.map { |field| record.send(field) }
100
+ cache_key_from_key_values(new_key_values)
101
+ end
102
+
103
+ def old_cache_key(record)
104
+ old_key_values = key_fields.map do |field|
105
+ field_string = field.to_s
106
+ changes = record.transaction_changed_attributes
107
+ if record.destroyed? && changes.key?(field_string)
108
+ changes[field_string]
109
+ elsif record.persisted? && changes.key?(field_string)
110
+ changes[field_string]
111
+ else
112
+ record.send(field)
113
+ end
114
+ end
115
+ cache_key_from_key_values(old_key_values)
116
+ end
117
+
118
+ def fetch_method_suffix
119
+ "#{alias_name}_by_#{key_fields.join('_and_')}"
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IdentityCache
4
+ module Cached
5
+ class AttributeByMulti < Attribute
6
+ def build
7
+ cached_attribute = self
8
+
9
+ model.define_singleton_method(:"fetch_#{fetch_method_suffix}") do |*key_values|
10
+ raise_if_scoped
11
+ cached_attribute.fetch(key_values)
12
+ end
13
+ end
14
+
15
+ private
16
+
17
+ # Attribute method overrides
18
+
19
+ def cast_db_key(key_values)
20
+ field_types.each_with_index do |type, i|
21
+ key_values[i] = type.cast(key_values[i])
22
+ end
23
+ key_values
24
+ end
25
+
26
+ def unhashed_values_cache_key_string(key_values)
27
+ key_values.map { |v| v.try!(:to_s).inspect }.join('/')
28
+ end
29
+
30
+ def load_from_db_where_conditions(key_values)
31
+ Hash[key_fields.zip(key_values)]
32
+ end
33
+
34
+ alias_method :cache_key_from_key_values, :cache_key
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ module IdentityCache
4
+ module Cached
5
+ class AttributeByOne < Attribute
6
+ attr_reader :key_field
7
+
8
+ def initialize(*)
9
+ super
10
+ @key_field = key_fields.first
11
+ end
12
+
13
+ def build
14
+ cached_attribute = self
15
+
16
+ model.define_singleton_method(:"fetch_#{fetch_method_suffix}") do |key|
17
+ raise_if_scoped
18
+ cached_attribute.fetch(key)
19
+ end
20
+
21
+ model.define_singleton_method(:"fetch_multi_#{fetch_method_suffix}") do |keys|
22
+ raise_if_scoped
23
+ cached_attribute.fetch_multi(keys)
24
+ end
25
+ end
26
+
27
+ def fetch_multi(keys)
28
+ keys = keys.map { |key| cast_db_key(key) }
29
+
30
+ unless model.should_use_cache?
31
+ return load_multi_from_db(keys)
32
+ end
33
+
34
+ unordered_hash = CacheKeyLoader.load_multi(self, keys)
35
+
36
+ # Calling `values` on the result is expected to return the values in the same order as their
37
+ # corresponding keys. The fetch_multi_by_#{field_list} generated methods depend on this.
38
+ ordered_hash = {}
39
+ keys.each { |key| ordered_hash[key] = unordered_hash.fetch(key) }
40
+ ordered_hash
41
+ end
42
+
43
+ def load_multi_from_db(keys)
44
+ rows = model.reorder(nil).where(load_from_db_where_conditions(keys)).pluck(key_field, attribute)
45
+ result = {}
46
+ default = unique ? nil : []
47
+ keys.each do |index_value|
48
+ result[index_value] = default.try!(:dup)
49
+ end
50
+ if unique
51
+ rows.each do |index_value, attribute_value|
52
+ result[index_value] = attribute_value
53
+ end
54
+ else
55
+ rows.each do |index_value, attribute_value|
56
+ result[index_value] << attribute_value
57
+ end
58
+ end
59
+ result
60
+ end
61
+
62
+ def cache_encode(db_value)
63
+ db_value
64
+ end
65
+ alias_method :cache_decode, :cache_encode
66
+
67
+ private
68
+
69
+ # Attribute method overrides
70
+
71
+ def cast_db_key(key)
72
+ field_types.first.cast(key)
73
+ end
74
+
75
+ def unhashed_values_cache_key_string(key)
76
+ key.try!(:to_s).inspect
77
+ end
78
+
79
+ def load_from_db_where_conditions(key_values)
80
+ { key_field => key_values }
81
+ end
82
+
83
+ def cache_key_from_key_values(key_values)
84
+ cache_key(key_values.first)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+ module IdentityCache
3
+ module Cached
4
+ class BelongsTo < Association # :nodoc:
5
+ attr_reader :records_variable_name
6
+
7
+ def build
8
+ reflection.active_record.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
9
+ def #{cached_accessor_name}
10
+ association_klass = association(:#{name}).klass
11
+ if association_klass.should_use_cache? && #{reflection.foreign_key}.present? && !association(:#{name}).loaded?
12
+ if defined?(#{records_variable_name})
13
+ #{records_variable_name}
14
+ else
15
+ #{records_variable_name} = association_klass.fetch_by_id(#{reflection.foreign_key})
16
+ end
17
+ else
18
+ #{name}
19
+ end
20
+ end
21
+ RUBY
22
+ end
23
+
24
+ def clear(record)
25
+ if record.instance_variable_defined?(records_variable_name)
26
+ record.remove_instance_variable(records_variable_name)
27
+ end
28
+ end
29
+
30
+ def fetch(records)
31
+ fetch_async(LoadStrategy::Eager, records) { |associated_records| associated_records }
32
+ end
33
+
34
+ def fetch_async(load_strategy, records)
35
+ if reflection.polymorphic?
36
+ cache_keys_to_associated_ids = {}
37
+
38
+ records.each do |owner_record|
39
+ associated_id = owner_record.send(reflection.foreign_key)
40
+ next unless associated_id && !owner_record.instance_variable_defined?(records_variable_name)
41
+ associated_cache_key = Object.const_get(
42
+ owner_record.send(reflection.foreign_type)
43
+ ).cached_model.cached_primary_index
44
+ unless cache_keys_to_associated_ids[associated_cache_key]
45
+ cache_keys_to_associated_ids[associated_cache_key] = {}
46
+ end
47
+ cache_keys_to_associated_ids[associated_cache_key][associated_id] = owner_record
48
+ end
49
+
50
+ load_strategy.load_batch(cache_keys_to_associated_ids) do |associated_records_by_cache_key|
51
+ batch_records = []
52
+ associated_records_by_cache_key.each do |cache_key, associated_records|
53
+ associated_records.keys.each do |id, associated_record|
54
+ owner_record = cache_keys_to_associated_ids.fetch(cache_key).fetch(id)
55
+ batch_records << owner_record
56
+ owner_record.instance_variable_set(records_variable_name, associated_record)
57
+ end
58
+ end
59
+
60
+ yield batch_records
61
+ end
62
+ else
63
+ ids_to_owner_record = records.each_with_object({}) do |owner_record, hash|
64
+ associated_id = owner_record.send(reflection.foreign_key)
65
+ if associated_id && !owner_record.instance_variable_defined?(records_variable_name)
66
+ hash[associated_id] = owner_record
67
+ end
68
+ end
69
+
70
+ load_strategy.load_multi(
71
+ reflection.klass.cached_primary_index,
72
+ ids_to_owner_record.keys
73
+ ) do |associated_records_by_id|
74
+ associated_records_by_id.each do |id, associated_record|
75
+ owner_record = ids_to_owner_record.fetch(id)
76
+ owner_record.instance_variable_set(records_variable_name, associated_record)
77
+ end
78
+
79
+ yield associated_records_by_id.values.compact
80
+ end
81
+ end
82
+ end
83
+
84
+ def embedded_recursively?
85
+ false
86
+ end
87
+
88
+ def embedded_by_reference?
89
+ false
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ module IdentityCache
3
+ module Cached
4
+ module EmbeddedFetching
5
+ private
6
+
7
+ def fetch_embedded(records)
8
+ fetch_embedded_async(LoadStrategy::Eager, records) {}
9
+ end
10
+
11
+ def fetch_embedded_async(load_strategy, records)
12
+ return yield if embedded_fetched?(records)
13
+
14
+ klass = reflection.active_record
15
+ cached_associations = klass.send(:embedded_associations)
16
+
17
+ return yield if cached_associations.empty?
18
+
19
+ return yield unless klass.primary_cache_index_enabled
20
+
21
+ load_strategy.load_multi(klass.cached_primary_index, records.map(&:id)) do |cached_records_by_id|
22
+ cached_associations.each_value do |cached_association|
23
+ records.each do |record|
24
+ next unless (cached_record = cached_records_by_id[record.id])
25
+ cached_value = cached_association.read(cached_record)
26
+ cached_association.write(record, cached_value)
27
+ end
28
+ end
29
+
30
+ yield
31
+ end
32
+ end
33
+
34
+ def embedded_fetched?(records)
35
+ # NOTE: Assume all records are the same, so just check the first one.
36
+ record = records.first
37
+ record.association(name).loaded? || record.instance_variable_defined?(records_variable_name)
38
+ end
39
+ end
40
+ end
41
+ end