redis-memo 0.0.0.beta.5 → 0.1.3

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee7d2004ee718072b93ece80d2e98b038b9f4a21cd73632c1df4a3609f11e9e4
4
- data.tar.gz: c8a89bb39f5b3e9f5a8b571942045c7b6b147a1d3e925f71ae611944c6aa13c9
3
+ metadata.gz: 9d4fa9d940a295287a258f88f3cbc44498a6e5475a58c0cdb8e5e320d2793652
4
+ data.tar.gz: bbe225fc22a568c3de716feba66b3a1fbbdeb52a1ef4c2c4a47b94bbfa8a33cb
5
5
  SHA512:
6
- metadata.gz: 15527040749e75dd0f27c31ecf08867cc63066b7582cdc4d618e269b2e9cb63e35601596d8f03900e55c192a20afd717c8b105426d5511df2c5db1906b4777e5
7
- data.tar.gz: 1ab368f33e5a17209e28efbe2086a067e9f016443b28d56ece0499f557e15fcb3d801930af535d3d1fe1e1eb125b8f2595e694a71a5476c862201e5886cb826c
6
+ metadata.gz: 3d5ec94adc1f74479690f5dc95eb7459b777de63827baa59302931287222266d33def750d9649c8c8e37a4fd43d95c7fac7c79b8ec012848f51ae5fad5f5ec91
7
+ data.tar.gz: 2219225dea50042b8f99dadcdad44f367d48c74573d3ac29ef0e4060c0dc7fee13a58034420229386bd16b408f6797718714e15af2710a954ad89964e7c21d5d
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
@@ -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?
@@ -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
@@ -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.5
4
+ version: 0.1.3
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: