redis-memo 0.0.0.beta.6 → 0.1.4

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: 10f721bc9b55f26c51674ce16f120bc643c306748dd9ff5aa5f54a7d9b944b51
4
- data.tar.gz: 3cf3fefe9a5119d231bf973da22aa7787fcac1cf71f46ab0cadc5421cc82da52
3
+ metadata.gz: a03c34771a13ad121b14359ba0e7b3596ef37da5cbf1f6c46e59bf28238e5e5d
4
+ data.tar.gz: 55d84f1b82f40c261b0ff8a7a5faa868b50caf248e89e755b31a0011f1caab3f
5
5
  SHA512:
6
- metadata.gz: a576fa80cef7462769055bdf657e0ee0f70fc9fb63a08f5b4ceabb931faaa8c6c8d310f01424c2124c7842b00d3c069d7c31499fc4dff166544e85b52989d0bd
7
- data.tar.gz: 45cc8638df93f0c22fd050ad39b25fd1d8671ac309fbee8e41979d51e0a6136e7c98d9fc9a31acd9e1e1828c592f5b2068349f6a0e1bb294664fcacea8911f48
6
+ metadata.gz: 83638771de1cb0041c4864b71d53bb89c99508f2827db23b504f1751e1ad22cff7010451d5f47cdcb14ffb619162f16fe4cf177e403b0522e654b7e697b0b872
7
+ data.tar.gz: fc5df8d6c8a336985927a2f35b177373a8f2e4dcd2b87bccac705440eaeaabe1a09f3d90138543c528b4763b70edc38931e6c9bc642e362b2c5ffef61e088d16
data/lib/redis_memo.rb CHANGED
@@ -19,6 +19,8 @@ module RedisMemo
19
19
 
20
20
  # @todo Move thread keys to +RedisMemo::ThreadKey+
21
21
  THREAD_KEY_WITHOUT_MEMO = :__redis_memo_without_memo__
22
+ THREAD_KEY_CONNECTION_ATTEMPTS_COUNT = :__redis_memo_connection_attempts_count__
23
+ THREAD_KEY_MAX_CONNECTION_ATTEMPTS = :__redis_memo_max_connection_attempts__
22
24
 
23
25
  # Configure global-level default options. Those options will be used unless
24
26
  # some options specified at +memoize_method+ callsite level. See
@@ -80,7 +82,7 @@ module RedisMemo
80
82
  #
81
83
  # @return [Boolean]
82
84
  def self.without_memo?
83
- Thread.current[THREAD_KEY_WITHOUT_MEMO] == true
85
+ ENV["REDIS_MEMO_DISABLE_ALL"] == 'true' || Thread.current[THREAD_KEY_WITHOUT_MEMO] == true
84
86
  end
85
87
 
86
88
  # Configure the wrapped code in the block to skip memoization.
@@ -94,6 +96,35 @@ module RedisMemo
94
96
  Thread.current[THREAD_KEY_WITHOUT_MEMO] = prev_value
95
97
  end
96
98
 
99
+ # Set the max connection attempts to Redis per code block. If we fail to connect to Redis more than `max_attempts`
100
+ # times, the rest of the code block will fall back to the uncached flow, `RedisMemo.without_memo`.
101
+ #
102
+ # @param [Integer] The max number of connection attempts.
103
+ # @yield [] no_args the block of code to set the max attempts for.
104
+ def self.with_max_connection_attempts(max_attempts)
105
+ prev_value = Thread.current[THREAD_KEY_WITHOUT_MEMO]
106
+ if max_attempts
107
+ Thread.current[THREAD_KEY_CONNECTION_ATTEMPTS_COUNT] = 0
108
+ Thread.current[THREAD_KEY_MAX_CONNECTION_ATTEMPTS] = max_attempts
109
+ end
110
+ yield
111
+ ensure
112
+ Thread.current[THREAD_KEY_WITHOUT_MEMO] = prev_value
113
+ Thread.current[THREAD_KEY_CONNECTION_ATTEMPTS_COUNT] = nil
114
+ Thread.current[THREAD_KEY_MAX_CONNECTION_ATTEMPTS] = nil
115
+ end
116
+
117
+ private
118
+ def self.incr_connection_attempts # :nodoc:
119
+ return if Thread.current[THREAD_KEY_MAX_CONNECTION_ATTEMPTS].nil? || Thread.current[THREAD_KEY_CONNECTION_ATTEMPTS_COUNT].nil?
120
+
121
+ # The connection attempts count and max connection attempts are reset in RedisMemo.with_max_connection_attempts
122
+ Thread.current[THREAD_KEY_CONNECTION_ATTEMPTS_COUNT] += 1
123
+ if Thread.current[THREAD_KEY_CONNECTION_ATTEMPTS_COUNT] >= Thread.current[THREAD_KEY_MAX_CONNECTION_ATTEMPTS]
124
+ Thread.current[THREAD_KEY_WITHOUT_MEMO] = true
125
+ end
126
+ end
127
+
97
128
  # @todo Move errors to a separate file errors.rb
98
129
  class ArgumentError < ::ArgumentError; end
99
130
  class RuntimeError < ::RuntimeError; end
@@ -17,6 +17,8 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
17
17
  RedisMemo::DefaultOptions.redis_error_handler&.call(exception, method)
18
18
  RedisMemo::DefaultOptions.logger&.warn(exception.full_message)
19
19
 
20
+ RedisMemo.incr_connection_attempts if exception.is_a?(Redis::BaseConnectionError)
21
+
20
22
  if Thread.current[THREAD_KEY_RAISE_ERROR]
21
23
  raise RedisMemo::Cache::Rescuable
22
24
  else
@@ -55,6 +57,16 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
55
57
  Thread.current[THREAD_KEY_LOCAL_DEPENDENCY_CACHE]
56
58
  end
57
59
 
60
+ # See https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/activesupport/lib/active_support/cache/redis_cache_store.rb#L477
61
+ # We overwrite this private method so we can also rescue ConnectionPool::TimeoutErrors
62
+ def failsafe(method, returning: nil)
63
+ yield
64
+ rescue ::Redis::BaseError, ::ConnectionPool::TimeoutError => e
65
+ handle_exception exception: e, method: method, returning: returning
66
+ returning
67
+ end
68
+ private :failsafe
69
+
58
70
  class << self
59
71
  def with_local_cache(&blk)
60
72
  Thread.current[THREAD_KEY_LOCAL_CACHE] = {}
@@ -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,24 @@ 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)
53
+ connection = ActiveRecord::Base.connection
54
+ unless connection.respond_to?(:dependency_of)
55
+ raise RedisMemo::WithoutMemoization, 'Caching active record queries is currently disabled on RedisMemo'
56
+ end
47
57
  # Extract the dependent memos of an Arel without calling exec_query to actually execute the query
48
58
  RedisMemo::MemoizeQuery::CachedSelect.with_new_query_context do
49
- connection = ActiveRecord::Base.connection
50
59
  query, binds, _ = connection.send(:to_sql_and_binds, relation.arel)
51
60
  RedisMemo::MemoizeQuery::CachedSelect.current_query = relation.arel
52
61
  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
62
 
61
- class UsingActiveRecord
62
- def self.===(dependency)
63
- RedisMemo::MemoizeQuery.using_active_record?(dependency)
63
+ unless is_query_cached
64
+ raise RedisMemo::WithoutMemoization, 'Arel query is not cached using RedisMemo'
65
+ end
66
+
67
+ connection.dependency_of(:exec_query, query, nil, binds)
64
68
  end
65
69
  end
66
70
  end
@@ -127,7 +127,10 @@ module RedisMemo::Memoizable::Invalidation
127
127
  begin
128
128
  bump_version(task)
129
129
  rescue SignalException, Redis::BaseConnectionError,
130
- ::ConnectionPool::TimeoutError
130
+ ::ConnectionPool::TimeoutError => e
131
+
132
+ RedisMemo::DefaultOptions.redis_error_handler&.call(e, __method__)
133
+ RedisMemo::DefaultOptions.logger&.warn(e.full_message)
131
134
  retry_queue << task
132
135
  end
133
136
  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|
@@ -13,6 +13,7 @@ if defined?(ActiveRecord)
13
13
  # after each record save
14
14
  def memoize_table_column(*raw_columns, editable: true)
15
15
  RedisMemo::MemoizeQuery.using_active_record!(self)
16
+ return if ENV["REDIS_MEMO_DISABLE_ALL"] == 'true'
16
17
  return if ENV["REDIS_MEMO_DISABLE_#{self.table_name.upcase}"] == 'true'
17
18
 
18
19
  columns = raw_columns.map(&:to_sym).sort
@@ -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?
@@ -346,6 +336,11 @@ class RedisMemo::MemoizeQuery::CachedSelect
346
336
  end
347
337
 
348
338
  bind_params
339
+
340
+ when Arel::Nodes::NotEqual
341
+ # We don't cache based on NOT queries (where.not) because it is unbound
342
+ # but we memoize queries with NOT and other bound queries, so we return the original bind_params
343
+ return bind_params
349
344
  else
350
345
  # Not yet supported
351
346
  return
@@ -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,104 +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
- model_class,
130
- records,
131
- columns_to_update,
132
- )
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]
133
205
  else
134
- []
206
+ # Ignore duplicate rows
207
+ nil
135
208
  end
136
209
 
137
- result = send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
138
-
139
- # Offload the records to invalidate while selecting the next set of
140
- # records to invalidate
141
- case records_to_invalidate
142
- when Array
143
- RedisMemo::MemoizeQuery.invalidate(*records_to_invalidate) unless records_to_invalidate.empty?
144
-
145
- RedisMemo::MemoizeQuery.invalidate(*RedisMemo::MemoizeQuery::Invalidation.send(
146
- :select_by_id,
147
- model_class,
148
- # Not all databases support "RETURNING", which is useful when
149
- # invaldating records after bulk creation
150
- result.ids,
151
- ))
152
- else
153
- RedisMemo::MemoizeQuery.invalidate_all(model_class)
210
+ if conflict_target && records.last.is_a?(Hash)
211
+ records.map! { |hash| model_class.new(hash) }
154
212
  end
155
213
 
156
- 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
157
221
  end
158
222
  end
159
223
  end
160
224
 
161
- def self.select_by_columns(model_class, records, columns_to_update)
162
- return [] if records.empty?
163
-
225
+ def self.build_relation_by_conflict_target(model_class, records, conflict_target)
164
226
  or_chain = nil
165
- columns_to_select = columns_to_update & RedisMemo::MemoizeQuery
166
- .memoized_columns(model_class)
167
- .to_a.flatten.uniq
168
-
169
- # Nothing to invalidate here
170
- return [] if columns_to_select.empty?
171
227
 
172
- RedisMemo::Tracer.trace(
173
- 'redis_memo.memoize_query.invalidation',
174
- "#{__method__}##{model_class.name}",
175
- ) do
176
- records.each do |record|
177
- conditions = {}
178
- columns_to_select.each do |column|
179
- conditions[column] = record.send(column)
180
- end
181
- if or_chain
182
- or_chain = or_chain.or(model_class.where(conditions))
183
- else
184
- or_chain = model_class.where(conditions)
185
- end
228
+ records.each do |record|
229
+ conditions = {}
230
+ conflict_target.each do |column|
231
+ conditions[column] = record.send(column)
186
232
  end
187
-
188
- record_count = RedisMemo.without_memo { or_chain.count }
189
- if record_count > bulk_operations_invalidation_limit
190
- nil
233
+ if or_chain
234
+ or_chain = or_chain.or(model_class.where(conditions))
191
235
  else
192
- RedisMemo.without_memo { or_chain.to_a }
236
+ or_chain = model_class.where(conditions)
193
237
  end
194
238
  end
239
+
240
+ or_chain
195
241
  end
196
242
 
197
- def self.select_by_id(model_class, ids)
243
+ def self.select_by_new_ids(model_class, target_id)
198
244
  RedisMemo::Tracer.trace(
199
245
  'redis_memo.memoize_query.invalidation',
200
246
  "#{__method__}##{model_class.name}",
201
247
  ) do
202
248
  RedisMemo.without_memo do
203
- 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
204
252
  end
205
253
  end
206
254
  end
207
255
 
208
- def self.bulk_operations_invalidation_limit
209
- ENV['REDIS_MEMO_BULK_OPERATIONS_INVALIDATION_LIMIT']&.to_i ||
210
- RedisMemo::DefaultOptions.bulk_operations_invalidation_limit ||
211
- 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
212
265
  end
213
266
  end
@@ -9,7 +9,9 @@ class RedisMemo::Middleware
9
9
  result = nil
10
10
 
11
11
  RedisMemo::Cache.with_local_cache do
12
- result = @app.call(env)
12
+ RedisMemo.with_max_connection_attempts(ENV['REDIS_MEMO_MAX_ATTEMPTS_PER_REQUEST']&.to_i) do
13
+ result = @app.call(env)
14
+ end
13
15
  end
14
16
  RedisMemo::Memoizable::Invalidation.drain_invalidation_queue
15
17
 
@@ -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
@@ -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.6
4
+ version: 0.1.4
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: