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