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: 10f721bc9b55f26c51674ce16f120bc643c306748dd9ff5aa5f54a7d9b944b51
4
- data.tar.gz: 3cf3fefe9a5119d231bf973da22aa7787fcac1cf71f46ab0cadc5421cc82da52
3
+ metadata.gz: dd1d436b41e1f30d0d0910d086811f675b87b6bbe799a0087da7453b4c016b66
4
+ data.tar.gz: 7ca970165cb321e0016d6f70f6135868df19b68d27a180084f8053f94bf780c0
5
5
  SHA512:
6
- metadata.gz: a576fa80cef7462769055bdf657e0ee0f70fc9fb63a08f5b4ceabb931faaa8c6c8d310f01424c2124c7842b00d3c069d7c31499fc4dff166544e85b52989d0bd
7
- data.tar.gz: 45cc8638df93f0c22fd050ad39b25fd1d8671ac309fbee8e41979d51e0a6136e7c98d9fc9a31acd9e1e1828c592f5b2068349f6a0e1bb294664fcacea8911f48
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 = 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,21 @@ 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)
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
- class UsingActiveRecord
62
- def self.===(dependency)
63
- RedisMemo::MemoizeQuery.using_active_record?(dependency)
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
- 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|
@@ -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,
@@ -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,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
- 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
- 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
- result = send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
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
- 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
157
221
  end
158
222
  end
159
223
  end
160
224
 
161
- def self.select_by_columns(model_class, records, columns_to_update)
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
- # Nothing to invalidate here
170
- return [] if columns_to_select.empty?
171
-
172
- RedisMemo::Tracer.trace(
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
- record_count = RedisMemo.without_memo { or_chain.count }
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
- RedisMemo.without_memo { or_chain.to_a }
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.select_by_id(model_class, ids)
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(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
204
252
  end
205
253
  end
206
254
  end
207
255
 
208
- def self.bulk_operations_invalidation_limit
209
- ENV['REDIS_MEMO_BULK_OPERATIONS_INVALIDATION_LIMIT']&.to_i ||
210
- RedisMemo::DefaultOptions.bulk_operations_invalidation_limit ||
211
- 10000
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
@@ -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
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.6
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: 1.3.1
213
+ version: '0'
214
214
  requirements: []
215
215
  rubygems_version: 3.0.8
216
216
  signing_key: