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,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