redis-memo 0.0.0.beta.6 → 0.1.0
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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: dd1d436b41e1f30d0d0910d086811f675b87b6bbe799a0087da7453b4c016b66
|
4
|
+
data.tar.gz: 7ca970165cb321e0016d6f70f6135868df19b68d27a180084f8053f94bf780c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d79af32bc2a55a3755072cd0502d355745c7f060241d6a7f5e116eab8c0b7a45b107d02ef5d4bbcec0e6acb5bf435f1bb4d6b50084d13e410307610ef675f420
|
7
|
+
data.tar.gz: d9e0714a72c526be0043185dfc2c0dc3f6a6c94557da5b81e31905616b1a156cb6d2c89934548332802fff42a24ed53c1518a97b3912d2cc8beae41d9dffc7bf
|
@@ -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,21 @@ class RedisMemo::Memoizable::Dependency
|
|
43
47
|
end
|
44
48
|
end
|
45
49
|
|
46
|
-
|
50
|
+
private
|
51
|
+
|
52
|
+
def self.extract_from_relation(relation)
|
47
53
|
# Extract the dependent memos of an Arel without calling exec_query to actually execute the query
|
48
54
|
RedisMemo::MemoizeQuery::CachedSelect.with_new_query_context do
|
49
55
|
connection = ActiveRecord::Base.connection
|
50
56
|
query, binds, _ = connection.send(:to_sql_and_binds, relation.arel)
|
51
57
|
RedisMemo::MemoizeQuery::CachedSelect.current_query = relation.arel
|
52
58
|
is_query_cached = RedisMemo::MemoizeQuery::CachedSelect.extract_bind_params(query)
|
53
|
-
raise(
|
54
|
-
RedisMemo::WithoutMemoization,
|
55
|
-
"Arel query is not cached using RedisMemo."
|
56
|
-
) unless is_query_cached
|
57
|
-
extracted_dependency = connection.dependency_of(:exec_query, query, nil, binds)
|
58
|
-
end
|
59
|
-
end
|
60
59
|
|
61
|
-
|
62
|
-
|
63
|
-
|
60
|
+
unless is_query_cached
|
61
|
+
raise RedisMemo::WithoutMemoization, 'Arel query is not cached using RedisMemo'
|
62
|
+
end
|
63
|
+
|
64
|
+
connection.dependency_of(:exec_query, query, nil, binds)
|
64
65
|
end
|
65
66
|
end
|
66
67
|
end
|
@@ -81,14 +81,88 @@ module RedisMemo::MemoizeMethod
|
|
81
81
|
|
82
82
|
def self.get_or_extract_dependencies(ref, *method_args, &depends_on)
|
83
83
|
if RedisMemo::Cache.local_dependency_cache
|
84
|
-
RedisMemo::Cache.local_dependency_cache[ref] ||= {}
|
85
|
-
RedisMemo::Cache.local_dependency_cache[ref][depends_on] ||= {}
|
86
|
-
|
84
|
+
RedisMemo::Cache.local_dependency_cache[ref.class] ||= {}
|
85
|
+
RedisMemo::Cache.local_dependency_cache[ref.class][depends_on] ||= {}
|
86
|
+
named_args = exclude_anonymous_args(depends_on, ref, method_args)
|
87
|
+
RedisMemo::Cache.local_dependency_cache[ref.class][depends_on][named_args] ||= extract_dependencies(ref, *method_args, &depends_on)
|
87
88
|
else
|
88
89
|
extract_dependencies(ref, *method_args, &depends_on)
|
89
90
|
end
|
90
91
|
end
|
91
92
|
|
93
|
+
# We only look at named method parameters in the dependency block in order to define its dependent
|
94
|
+
# memos and ignore anonymous parameters, following the convention that nil or :_ is an anonymous parameter.
|
95
|
+
# Example:
|
96
|
+
# ```
|
97
|
+
# def method(param1, param2)
|
98
|
+
# end
|
99
|
+
#
|
100
|
+
# memoize_method :method do |_, _, param2|`
|
101
|
+
# depends_on RedisMemo::Memoizable.new(param2: param2)
|
102
|
+
# end
|
103
|
+
# ```
|
104
|
+
# `exclude_anonymous_args(depends_on, ref, [1, 2])` returns [2]
|
105
|
+
def self.exclude_anonymous_args(depends_on, ref, args)
|
106
|
+
return [] if depends_on.parameters.empty? or args.empty?
|
107
|
+
|
108
|
+
positional_args = []
|
109
|
+
kwargs = {}
|
110
|
+
depends_on_args = [ref] + args
|
111
|
+
options = depends_on_args.extract_options!
|
112
|
+
|
113
|
+
# Keep track of the splat start index, and the number of positional args before and after the splat,
|
114
|
+
# so we can map which args belong to positional args and which args belong to the splat.
|
115
|
+
named_splat = false
|
116
|
+
splat_index = nil
|
117
|
+
num_positional_args_after_splat = 0
|
118
|
+
num_positional_args_before_splat = 0
|
119
|
+
|
120
|
+
depends_on.parameters.each_with_index do |param, i|
|
121
|
+
# Defined by https://github.com/ruby/ruby/blob/22b8ddfd1049c3fd1e368684c4fd03bceb041b3a/proc.c#L3048-L3059
|
122
|
+
case param.first
|
123
|
+
when :opt, :req
|
124
|
+
if splat_index
|
125
|
+
num_positional_args_after_splat += 1
|
126
|
+
else
|
127
|
+
num_positional_args_before_splat += 1
|
128
|
+
end
|
129
|
+
when :rest
|
130
|
+
named_splat = is_named?(param)
|
131
|
+
splat_index = i
|
132
|
+
when :key, :keyreq
|
133
|
+
kwargs[param.last] = options[param.last] if is_named?(param)
|
134
|
+
when :keyrest
|
135
|
+
kwargs.merge!(options) if is_named?(param)
|
136
|
+
else
|
137
|
+
raise(RedisMemo::ArgumentError, "#{param.first} argument isn't supported in the dependency block")
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# Determine the named positional and splat arguments after we know the # of pos. arguments before and after splat
|
142
|
+
after_splat_index = depends_on_args.size - num_positional_args_after_splat
|
143
|
+
depends_on_args.each_with_index do |arg, i|
|
144
|
+
# if the index is within the splat
|
145
|
+
if i >= num_positional_args_before_splat && i < after_splat_index
|
146
|
+
positional_args << arg if named_splat
|
147
|
+
else
|
148
|
+
j = i < num_positional_args_before_splat ? i : i - (after_splat_index - splat_index) - 1
|
149
|
+
positional_args << arg if is_named?(depends_on.parameters[j])
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
if !kwargs.empty?
|
154
|
+
positional_args + [kwargs]
|
155
|
+
elsif named_splat && !options.empty?
|
156
|
+
positional_args + [options]
|
157
|
+
else
|
158
|
+
positional_args
|
159
|
+
end
|
160
|
+
end
|
161
|
+
private
|
162
|
+
def self.is_named?(param)
|
163
|
+
param.size == 2 && param.last != :_
|
164
|
+
end
|
165
|
+
|
92
166
|
def self.method_cache_keys(future_contexts)
|
93
167
|
memos = Array.new(future_contexts.size)
|
94
168
|
future_contexts.each_with_index do |(_, _, dependent_memos), i|
|
@@ -111,15 +111,8 @@ class RedisMemo::MemoizeQuery::CachedSelect
|
|
111
111
|
sql.gsub(/(\$\d+)/, '?') # $1 -> ?
|
112
112
|
.gsub(/((, *)*\?)+/, '?') # (?, ?, ? ...) -> (?)
|
113
113
|
end,
|
114
|
-
) do |_, sql,
|
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,
|
@@ -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,73 @@ 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
227
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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
|
-
|
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
|
+
RedisMemo::Tracer.trace(
|
258
|
+
'redis_memo.memoize_query.invalidation',
|
259
|
+
"#{__method__}##{model_class.name}",
|
260
|
+
) do
|
261
|
+
RedisMemo.without_memo { relation.reload }
|
262
|
+
end
|
212
263
|
end
|
213
264
|
end
|
data/lib/redis_memo/options.rb
CHANGED
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.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Chan Zuckerberg Initiative
|
@@ -208,9 +208,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
208
208
|
version: 2.5.0
|
209
209
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
210
210
|
requirements:
|
211
|
-
- - "
|
211
|
+
- - ">="
|
212
212
|
- !ruby/object:Gem::Version
|
213
|
-
version:
|
213
|
+
version: '0'
|
214
214
|
requirements: []
|
215
215
|
rubygems_version: 3.0.8
|
216
216
|
signing_key:
|