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 +4 -4
- data/lib/redis_memo.rb +32 -1
- data/lib/redis_memo/cache.rb +12 -0
- data/lib/redis_memo/memoizable/dependency.rb +22 -18
- data/lib/redis_memo/memoizable/invalidation.rb +4 -1
- data/lib/redis_memo/memoize_method.rb +77 -3
- data/lib/redis_memo/memoize_query.rb +1 -0
- data/lib/redis_memo/memoize_query/cached_select.rb +7 -12
- data/lib/redis_memo/memoize_query/invalidation.rb +133 -80
- data/lib/redis_memo/middleware.rb +3 -1
- data/lib/redis_memo/options.rb +0 -1
- data/lib/redis_memo/testing.rb +55 -0
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a03c34771a13ad121b14359ba0e7b3596ef37da5cbf1f6c46e59bf28238e5e5d
|
4
|
+
data.tar.gz: 55d84f1b82f40c261b0ff8a7a5faa868b50caf248e89e755b31a0011f1caab3f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
data/lib/redis_memo/cache.rb
CHANGED
@@ -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 =
|
29
|
+
extracted = self.class.extract_from_relation(dependency)
|
30
30
|
nodes.merge!(extracted.nodes)
|
31
|
-
when
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
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,
|
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.
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
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
|
-
|
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
|
-
|
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.
|
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(
|
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.
|
209
|
-
|
210
|
-
|
211
|
-
|
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
|
-
|
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
|
|
data/lib/redis_memo/options.rb
CHANGED
@@ -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.
|
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:
|
214
|
+
version: '0'
|
214
215
|
requirements: []
|
215
216
|
rubygems_version: 3.0.8
|
216
217
|
signing_key:
|