redis-memo 0.0.0.beta.6 → 0.1.4
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 +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:
|