redis-memo 0.0.0.beta.4 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 72bcf0679b811825222a6b38de0fcdadf4546640d2f61b18ebc827e37137dc41
4
- data.tar.gz: 7a55c7bbd084a24afe085e85bc5cd5c486cf240094b64ca09d48e9e0ce126384
3
+ metadata.gz: 976988ff54686693396ffb07c47b9e4326646837b3be53264891c886f03771c6
4
+ data.tar.gz: 9e4a3d4dca78cb9389af8e1728addb3aa8d04493365bcfda2ea236e20c657971
5
5
  SHA512:
6
- metadata.gz: 5a3af887512e4b6571829fcfda719e43abb917eb4773407bd27788b0b671a42263ec31c2f6286f0a1279a354851c8c8cb48eb9701cba59863e500ca47419080e
7
- data.tar.gz: feeb72b177ec07953b839e6e0759cbdf42dfd4eddd19f89599e721589462e849e7685a659b03128504d670764223f1805402cb3ee8a96411b834f229ba837d99
6
+ metadata.gz: 118d801be413e14616becfb3a9c4e4715a1e7b7cb6722ac48bd8b1c63e6b1c8f565c8a148909b4bd7b4b538f89d1d79145ad137403139eb7158cad61c9d7a847
7
+ data.tar.gz: 52bdf762fc8bf22a6b65fc158512e001dd12490904db9766cd9f3ff1aee74e61f23f218bfa46cdf57fdb67f9f0d2508649fedc2379d6224d0d34416ffbafb902
@@ -26,14 +26,18 @@ class RedisMemo::Memoizable::Dependency
26
26
  instance_exec(&memo.depends_on)
27
27
  end
28
28
  when ActiveRecord::Relation
29
- extracted = extract_dependencies_for_relation(dependency)
29
+ extracted = self.class.extract_from_relation(dependency)
30
30
  nodes.merge!(extracted.nodes)
31
- when UsingActiveRecord
32
- [
33
- dependency.redis_memo_class_memoizable,
34
- RedisMemo::MemoizeQuery.create_memo(dependency, **conditions),
35
- ].each do |memo|
31
+ when RedisMemo::MemoizeQuery::CachedSelect::BindParams
32
+ # A private API
33
+ dependency.params.each do |model, attrs_set|
34
+ memo = model.redis_memo_class_memoizable
36
35
  nodes[memo.cache_key] = memo
36
+
37
+ attrs_set.each do |attrs|
38
+ memo = RedisMemo::MemoizeQuery.create_memo(model, **attrs)
39
+ nodes[memo.cache_key] = memo
40
+ end
37
41
  end
38
42
  else
39
43
  raise(
@@ -43,24 +47,21 @@ class RedisMemo::Memoizable::Dependency
43
47
  end
44
48
  end
45
49
 
46
- def extract_dependencies_for_relation(relation)
50
+ private
51
+
52
+ def self.extract_from_relation(relation)
47
53
  # Extract the dependent memos of an Arel without calling exec_query to actually execute the query
48
54
  RedisMemo::MemoizeQuery::CachedSelect.with_new_query_context do
49
55
  connection = ActiveRecord::Base.connection
50
56
  query, binds, _ = connection.send(:to_sql_and_binds, relation.arel)
51
57
  RedisMemo::MemoizeQuery::CachedSelect.current_query = relation.arel
52
58
  is_query_cached = RedisMemo::MemoizeQuery::CachedSelect.extract_bind_params(query)
53
- raise(
54
- RedisMemo::WithoutMemoization,
55
- "Arel query is not cached using RedisMemo."
56
- ) unless is_query_cached
57
- extracted_dependency = connection.dependency_of(:exec_query, query, nil, binds)
58
- end
59
- end
60
59
 
61
- class UsingActiveRecord
62
- def self.===(dependency)
63
- RedisMemo::MemoizeQuery.using_active_record?(dependency)
60
+ unless is_query_cached
61
+ raise RedisMemo::WithoutMemoization, 'Arel query is not cached using RedisMemo'
62
+ end
63
+
64
+ connection.dependency_of(:exec_query, query, nil, binds)
64
65
  end
65
66
  end
66
67
  end
@@ -81,14 +81,88 @@ module RedisMemo::MemoizeMethod
81
81
 
82
82
  def self.get_or_extract_dependencies(ref, *method_args, &depends_on)
83
83
  if RedisMemo::Cache.local_dependency_cache
84
- RedisMemo::Cache.local_dependency_cache[ref] ||= {}
85
- RedisMemo::Cache.local_dependency_cache[ref][depends_on] ||= {}
86
- RedisMemo::Cache.local_dependency_cache[ref][depends_on][method_args] ||= extract_dependencies(ref, *method_args, &depends_on)
84
+ RedisMemo::Cache.local_dependency_cache[ref.class] ||= {}
85
+ RedisMemo::Cache.local_dependency_cache[ref.class][depends_on] ||= {}
86
+ named_args = exclude_anonymous_args(depends_on, ref, method_args)
87
+ RedisMemo::Cache.local_dependency_cache[ref.class][depends_on][named_args] ||= extract_dependencies(ref, *method_args, &depends_on)
87
88
  else
88
89
  extract_dependencies(ref, *method_args, &depends_on)
89
90
  end
90
91
  end
91
92
 
93
+ # We only look at named method parameters in the dependency block in order to define its dependent
94
+ # memos and ignore anonymous parameters, following the convention that nil or :_ is an anonymous parameter.
95
+ # Example:
96
+ # ```
97
+ # def method(param1, param2)
98
+ # end
99
+ #
100
+ # memoize_method :method do |_, _, param2|`
101
+ # depends_on RedisMemo::Memoizable.new(param2: param2)
102
+ # end
103
+ # ```
104
+ # `exclude_anonymous_args(depends_on, ref, [1, 2])` returns [2]
105
+ def self.exclude_anonymous_args(depends_on, ref, args)
106
+ return [] if depends_on.parameters.empty? or args.empty?
107
+
108
+ positional_args = []
109
+ kwargs = {}
110
+ depends_on_args = [ref] + args
111
+ options = depends_on_args.extract_options!
112
+
113
+ # Keep track of the splat start index, and the number of positional args before and after the splat,
114
+ # so we can map which args belong to positional args and which args belong to the splat.
115
+ named_splat = false
116
+ splat_index = nil
117
+ num_positional_args_after_splat = 0
118
+ num_positional_args_before_splat = 0
119
+
120
+ depends_on.parameters.each_with_index do |param, i|
121
+ # Defined by https://github.com/ruby/ruby/blob/22b8ddfd1049c3fd1e368684c4fd03bceb041b3a/proc.c#L3048-L3059
122
+ case param.first
123
+ when :opt, :req
124
+ if splat_index
125
+ num_positional_args_after_splat += 1
126
+ else
127
+ num_positional_args_before_splat += 1
128
+ end
129
+ when :rest
130
+ named_splat = is_named?(param)
131
+ splat_index = i
132
+ when :key, :keyreq
133
+ kwargs[param.last] = options[param.last] if is_named?(param)
134
+ when :keyrest
135
+ kwargs.merge!(options) if is_named?(param)
136
+ else
137
+ raise(RedisMemo::ArgumentError, "#{param.first} argument isn't supported in the dependency block")
138
+ end
139
+ end
140
+
141
+ # Determine the named positional and splat arguments after we know the # of pos. arguments before and after splat
142
+ after_splat_index = depends_on_args.size - num_positional_args_after_splat
143
+ depends_on_args.each_with_index do |arg, i|
144
+ # if the index is within the splat
145
+ if i >= num_positional_args_before_splat && i < after_splat_index
146
+ positional_args << arg if named_splat
147
+ else
148
+ j = i < num_positional_args_before_splat ? i : i - (after_splat_index - splat_index) - 1
149
+ positional_args << arg if is_named?(depends_on.parameters[j])
150
+ end
151
+ end
152
+
153
+ if !kwargs.empty?
154
+ positional_args + [kwargs]
155
+ elsif named_splat && !options.empty?
156
+ positional_args + [options]
157
+ else
158
+ positional_args
159
+ end
160
+ end
161
+ private
162
+ def self.is_named?(param)
163
+ param.size == 2 && param.last != :_
164
+ end
165
+
92
166
  def self.method_cache_keys(future_contexts)
93
167
  memos = Array.new(future_contexts.size)
94
168
  future_contexts.each_with_index do |(_, _, dependent_memos), i|
@@ -111,15 +111,8 @@ class RedisMemo::MemoizeQuery::CachedSelect
111
111
  sql.gsub(/(\$\d+)/, '?') # $1 -> ?
112
112
  .gsub(/((, *)*\?)+/, '?') # (?, ?, ? ...) -> (?)
113
113
  end,
114
- ) do |_, sql, name, binds, **kwargs|
115
- RedisMemo::MemoizeQuery::CachedSelect
116
- .current_query_bind_params
117
- .params
118
- .each do |model, attrs_set|
119
- attrs_set.each do |attrs|
120
- depends_on model, **attrs
121
- end
122
- end
114
+ ) do |_, sql, _, binds, **|
115
+ depends_on RedisMemo::MemoizeQuery::CachedSelect.current_query_bind_params
123
116
 
124
117
  depends_on RedisMemo::Memoizable.new(
125
118
  __redis_memo_memoize_query_memoize_query_sql__: sql,
@@ -274,9 +267,6 @@ class RedisMemo::MemoizeQuery::CachedSelect
274
267
 
275
268
  bind_params
276
269
  when Arel::Nodes::SelectStatement
277
- # No OREDER BY
278
- return unless node.orders.empty?
279
-
280
270
  node.cores.each do |core|
281
271
  # We don't support JOINs
282
272
  return unless core.source.right.empty?
@@ -36,10 +36,8 @@ class RedisMemo::MemoizeQuery::Invalidation
36
36
  decrement_counter
37
37
  delete_all delete_by
38
38
  increment_counter
39
- insert insert! insert_all insert_all!
40
39
  touch_all
41
40
  update_column update_columns update_all update_counters
42
- upsert upsert_all
43
41
  ).each do |method_name|
44
42
  # Example: Model.update_all
45
43
  rewrite_default_method(
@@ -58,6 +56,24 @@ class RedisMemo::MemoizeQuery::Invalidation
58
56
  )
59
57
  end
60
58
 
59
+ %i(
60
+ insert insert! insert_all insert_all!
61
+ ).each do |method_name|
62
+ rewrite_insert_method(
63
+ model_class,
64
+ method_name,
65
+ )
66
+ end
67
+
68
+ %i(
69
+ upsert upsert_all
70
+ ).each do |method_name|
71
+ rewrite_upsert_method(
72
+ model_class,
73
+ method_name,
74
+ )
75
+ end
76
+
61
77
  %i(
62
78
  import import!
63
79
  ).each do |method_name|
@@ -70,6 +86,36 @@ class RedisMemo::MemoizeQuery::Invalidation
70
86
  model_class.class_variable_set(var_name, true)
71
87
  end
72
88
 
89
+ def self.invalidate_new_records(model_class, &blk)
90
+ current_id = model_class.maximum(model_class.primary_key)
91
+ result = blk.call
92
+ records = select_by_new_ids(model_class, current_id)
93
+ RedisMemo::MemoizeQuery.invalidate(*records) unless records.empty?
94
+ result
95
+ end
96
+
97
+ def self.invalidate_records_by_conflict_target(model_class, records:, conflict_target: nil, &blk)
98
+ if conflict_target.nil?
99
+ # When the conflict_target is not set, we are basically inserting new
100
+ # records since duplicate rows are simply skipped
101
+ return invalidate_new_records(model_class, &blk)
102
+ end
103
+
104
+ relation = build_relation_by_conflict_target(model_class, records, conflict_target)
105
+ # Invalidate records before updating
106
+ records = select_by_conflict_target_relation(model_class, relation)
107
+ RedisMemo::MemoizeQuery.invalidate(*records) unless records.empty?
108
+
109
+ # Perform updating
110
+ result = blk.call
111
+
112
+ # Invalidate records after updating
113
+ records = select_by_conflict_target_relation(model_class, relation)
114
+ RedisMemo::MemoizeQuery.invalidate(*records) unless records.empty?
115
+
116
+ result
117
+ end
118
+
73
119
  private
74
120
 
75
121
  #
@@ -95,12 +141,48 @@ class RedisMemo::MemoizeQuery::Invalidation
95
141
  end
96
142
  end
97
143
 
98
- def self.rewrite_import_method(model_class, method_name)
99
- # This optimization to avoid over-invalidation only works on postgres
100
- unless ActiveRecord::Base.connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
101
- rewrite_default_method(model_class, model_class, method_name, class_method: true)
102
- return
144
+ def self.rewrite_insert_method(model_class, method_name)
145
+ return unless model_class.respond_to?(method_name)
146
+
147
+ model_class.singleton_class.class_eval do
148
+ alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
149
+
150
+ define_method method_name do |*args, &blk|
151
+ RedisMemo::MemoizeQuery::Invalidation.invalidate_new_records(model_class) do
152
+ send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
153
+ end
154
+ end
103
155
  end
156
+ end
157
+
158
+ def self.rewrite_upsert_method(model_class, method_name)
159
+ return unless model_class.respond_to?(method_name)
160
+
161
+ model_class.singleton_class.class_eval do
162
+ alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
163
+
164
+ define_method method_name do |attributes, unique_by: nil, **kwargs, &blk|
165
+ RedisMemo::MemoizeQuery::Invalidation.invalidate_records_by_conflict_target(
166
+ model_class,
167
+ records: nil, # not used
168
+ # upsert does not support on_duplicate_key_update yet at activerecord
169
+ # HEAD (6.1.3)
170
+ conflict_target: nil,
171
+ ) do
172
+ send(
173
+ :"#{method_name}_without_redis_memo_invalidation",
174
+ attributes,
175
+ unique_by: unique_by,
176
+ **kwargs,
177
+ &blk
178
+ )
179
+ end
180
+ end
181
+ end
182
+ end
183
+
184
+ def self.rewrite_import_method(model_class, method_name)
185
+ return unless model_class.respond_to?(method_name)
104
186
 
105
187
  model_class.singleton_class.class_eval do
106
188
  alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
@@ -110,102 +192,75 @@ class RedisMemo::MemoizeQuery::Invalidation
110
192
  define_method method_name do |*args, &blk|
111
193
  options = args.last.is_a?(Hash) ? args.last : {}
112
194
  records = args[args.last.is_a?(Hash) ? -2 : -1]
113
- columns_to_update = options[:on_duplicate_key_update]
114
- if columns_to_update.is_a?(Hash)
115
- columns_to_update = columns_to_update[:columns]
116
- end
117
-
118
- if records.last.is_a?(Hash)
119
- records.map! { |hash| model_class.new(hash) }
120
- end
121
-
122
- # Invalidate the records before and after the import to resolve
123
- # - default values filled by the database
124
- # - updates on conflict conditions
125
- records_to_invalidate =
126
- if columns_to_update
127
- RedisMemo::MemoizeQuery::Invalidation.send(
128
- :select_by_columns,
129
- records,
130
- columns_to_update,
131
- )
195
+ on_duplicate_key_update = options[:on_duplicate_key_update]
196
+ conflict_target =
197
+ case on_duplicate_key_update
198
+ when Hash
199
+ # The conflict_target option is only supported in PostgreSQL. In
200
+ # MySQL, the primary_key is used as the conflict_target
201
+ on_duplicate_key_update[:conflict_target] || [model_class.primary_key.to_sym]
202
+ when Array
203
+ # The default conflict_target is just the primary_key
204
+ [model_class.primary_key.to_sym]
132
205
  else
133
- []
206
+ # Ignore duplicate rows
207
+ nil
134
208
  end
135
209
 
136
- result = send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
137
-
138
- # Offload the records to invalidate while selecting the next set of
139
- # records to invalidate
140
- case records_to_invalidate
141
- when Array
142
- RedisMemo::MemoizeQuery.invalidate(*records_to_invalidate) unless records_to_invalidate.empty?
143
-
144
- RedisMemo::MemoizeQuery.invalidate(*RedisMemo::MemoizeQuery::Invalidation.send(
145
- :select_by_id,
146
- model_class,
147
- # Not all databases support "RETURNING", which is useful when
148
- # invaldating records after bulk creation
149
- result.ids,
150
- ))
151
- else
152
- RedisMemo::MemoizeQuery.invalidate_all(model_class)
210
+ if conflict_target && records.last.is_a?(Hash)
211
+ records.map! { |hash| model_class.new(hash) }
153
212
  end
154
213
 
155
- result
214
+ RedisMemo::MemoizeQuery::Invalidation.invalidate_records_by_conflict_target(
215
+ model_class,
216
+ records: records,
217
+ conflict_target: conflict_target,
218
+ ) do
219
+ send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
220
+ end
156
221
  end
157
222
  end
158
223
  end
159
224
 
160
- def self.select_by_columns(records, columns_to_update)
161
- model_class = records.first.class
225
+ def self.build_relation_by_conflict_target(model_class, records, conflict_target)
162
226
  or_chain = nil
163
- columns_to_select = columns_to_update & RedisMemo::MemoizeQuery
164
- .memoized_columns(model_class)
165
- .to_a.flatten.uniq
166
-
167
- # Nothing to invalidate here
168
- return [] if columns_to_select.empty?
169
227
 
170
- RedisMemo::Tracer.trace(
171
- 'redis_memo.memoize_query.invalidation',
172
- "#{__method__}##{model_class.name}",
173
- ) do
174
- records.each do |record|
175
- conditions = {}
176
- columns_to_select.each do |column|
177
- conditions[column] = record.send(column)
178
- end
179
- if or_chain
180
- or_chain = or_chain.or(model_class.where(conditions))
181
- else
182
- or_chain = model_class.where(conditions)
183
- end
228
+ records.each do |record|
229
+ conditions = {}
230
+ conflict_target.each do |column|
231
+ conditions[column] = record.send(column)
184
232
  end
185
-
186
- record_count = RedisMemo.without_memo { or_chain.count }
187
- if record_count > bulk_operations_invalidation_limit
188
- nil
233
+ if or_chain
234
+ or_chain = or_chain.or(model_class.where(conditions))
189
235
  else
190
- RedisMemo.without_memo { or_chain.to_a }
236
+ or_chain = model_class.where(conditions)
191
237
  end
192
238
  end
239
+
240
+ or_chain
193
241
  end
194
242
 
195
- def self.select_by_id(model_class, ids)
243
+ def self.select_by_new_ids(model_class, target_id)
196
244
  RedisMemo::Tracer.trace(
197
245
  'redis_memo.memoize_query.invalidation',
198
246
  "#{__method__}##{model_class.name}",
199
247
  ) do
200
248
  RedisMemo.without_memo do
201
- model_class.where(model_class.primary_key => ids).to_a
249
+ model_class.where(
250
+ model_class.arel_table[model_class.primary_key].gt(target_id),
251
+ ).to_a
202
252
  end
203
253
  end
204
254
  end
205
255
 
206
- def self.bulk_operations_invalidation_limit
207
- ENV['REDIS_MEMO_BULK_OPERATIONS_INVALIDATION_LIMIT']&.to_i ||
208
- RedisMemo::DefaultOptions.bulk_operations_invalidation_limit ||
209
- 10000
256
+ def self.select_by_conflict_target_relation(model_class, relation)
257
+ return [] unless relation
258
+
259
+ RedisMemo::Tracer.trace(
260
+ 'redis_memo.memoize_query.invalidation',
261
+ "#{__method__}##{model_class.name}",
262
+ ) do
263
+ RedisMemo.without_memo { relation.reload }
264
+ end
210
265
  end
211
266
  end
@@ -72,7 +72,6 @@ class RedisMemo::Options
72
72
  end
73
73
 
74
74
  attr_accessor :async
75
- attr_accessor :bulk_operations_invalidation_limit
76
75
  attr_accessor :cache_out_of_date_handler
77
76
  attr_accessor :cache_validation_sampler
78
77
  attr_accessor :compress
@@ -31,7 +31,8 @@ class RedisMemo::Redis < Redis::Distributed
31
31
  end
32
32
 
33
33
  class WithReplicas < ::Redis
34
- def initialize(options)
34
+ def initialize(orig_options)
35
+ options = orig_options.dup
35
36
  primary_option = options.shift
36
37
  @replicas = options.map do |option|
37
38
  option[:logger] ||= RedisMemo::DefaultOptions.logger
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Redis memo can be flaky due to transient network errors (e.g. Redis connection errors), or when
4
+ # used with async handlers. This class allows users to override the default redis-memo behavior
5
+ # to be more robust when testing their code that uses redis-memo.
6
+ module RedisMemo
7
+ class Testing
8
+
9
+ def self.__test_mode
10
+ @__test_mode
11
+ end
12
+
13
+ def self.__test_mode=(mode)
14
+ @__test_mode = mode
15
+ end
16
+
17
+ def self.enable_test_mode(&blk)
18
+ __set_test_mode(true, &blk)
19
+ end
20
+
21
+ def self.disable_test_mode(&blk)
22
+ __set_test_mode(false, &blk)
23
+ end
24
+
25
+ def self.enabled?
26
+ __test_mode
27
+ end
28
+
29
+ private
30
+
31
+ def self.__set_test_mode(mode, &blk)
32
+ if blk.nil?
33
+ __test_mode = mode
34
+ else
35
+ prev_mode = __test_mode
36
+ begin
37
+ __test_mode = mode
38
+ yield
39
+ ensure
40
+ __test_mode = prev_mode
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ module TestOverrides
47
+ def without_memo?
48
+ if RedisMemo::Testing.enabled? && !RedisMemo::Memoizable::Invalidation.class_variable_get(:@@invalidation_queue).empty?
49
+ return true
50
+ end
51
+ super
52
+ end
53
+ end
54
+ singleton_class.prepend(TestOverrides)
55
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis-memo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0.beta.4
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chan Zuckerberg Initiative
@@ -192,6 +192,7 @@ files:
192
192
  - lib/redis_memo/middleware.rb
193
193
  - lib/redis_memo/options.rb
194
194
  - lib/redis_memo/redis.rb
195
+ - lib/redis_memo/testing.rb
195
196
  - lib/redis_memo/tracer.rb
196
197
  homepage: https://github.com/chanzuckerberg/redis-memo
197
198
  licenses:
@@ -208,9 +209,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
208
209
  version: 2.5.0
209
210
  required_rubygems_version: !ruby/object:Gem::Requirement
210
211
  requirements:
211
- - - ">"
212
+ - - ">="
212
213
  - !ruby/object:Gem::Version
213
- version: 1.3.1
214
+ version: '0'
214
215
  requirements: []
215
216
  rubygems_version: 3.0.8
216
217
  signing_key: