redis-memo 0.0.0.beta.3 → 0.1.1

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: fd8d075c12b20782f27bdfe2488cba1c0f31f5fb537241a9cedcc72f484588b9
4
- data.tar.gz: 0eef678b83746557ba84aca013e660ecc31a028042cf052606673179e185d61e
3
+ metadata.gz: c1f9810ab293851487a23911835eb644ce4401df360404a04236898b99e1ab64
4
+ data.tar.gz: edbd814f8e8c027207123340445f121e771e30260e7126fb91665a23139fd710
5
5
  SHA512:
6
- metadata.gz: a8f573eb77832f3dfb0600ab607c8d60090969082c86a9403a98b784899931da60d66f1b761bf863fd4007492ff47dee2d7880ec2f10cf5143fce33bc791684b
7
- data.tar.gz: 4f9e44c13d2b4918f69f143f78eb4d80ad306b43abb650c03c481d727a88938f70c3ef89854e5c67afe92cc656df82ff44df84bb07878904bcdfbb2b86c15275
6
+ metadata.gz: 2a07892d98f25a3690ee254015be04f0164053be755fcce73478598ba65e28a39a4e99b47d18b12f1ec6e89facfba06e9d1b8390dab61ac3713e291f4451b782
7
+ data.tar.gz: 47f5e0d42b7957d6422277f01446b6659f8526371e556957626a68c44635d1ba456380152d48f3682959cbd84f4d2adf333a50697be386f554b000b4edf67d1f
data/lib/redis_memo.rb CHANGED
@@ -97,4 +97,5 @@ module RedisMemo
97
97
  # @todo Move errors to a separate file errors.rb
98
98
  class ArgumentError < ::ArgumentError; end
99
99
  class RuntimeError < ::RuntimeError; end
100
+ class WithoutMemoization < Exception; end
100
101
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
  require_relative 'options'
3
3
  require_relative 'redis'
4
+ require_relative 'connection_pool'
4
5
 
5
6
  class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
6
7
  class Rescuable < Exception; end
@@ -24,7 +25,12 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
24
25
  end
25
26
 
26
27
  def self.redis
27
- @@redis ||= RedisMemo::DefaultOptions.redis
28
+ @@redis ||=
29
+ if RedisMemo::DefaultOptions.connection_pool
30
+ RedisMemo::ConnectionPool.new(**RedisMemo::DefaultOptions.connection_pool)
31
+ else
32
+ RedisMemo::DefaultOptions.redis
33
+ end
28
34
  end
29
35
 
30
36
  def self.redis_store
@@ -60,7 +66,7 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
60
66
  end
61
67
 
62
68
  # RedisCacheStore doesn't read from the local cache before reading from redis
63
- def read_multi(*keys, raise_error: false)
69
+ def read_multi(*keys, raw: false, raise_error: false)
64
70
  return {} if keys.empty?
65
71
 
66
72
  Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
@@ -71,7 +77,7 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
71
77
  keys_to_fetch -= local_entries.keys unless local_entries.empty?
72
78
  return local_entries if keys_to_fetch.empty?
73
79
 
74
- remote_entries = redis_store.read_multi(*keys_to_fetch)
80
+ remote_entries = redis_store.read_multi(*keys_to_fetch, raw: raw)
75
81
  local_cache&.merge!(remote_entries)
76
82
 
77
83
  if local_entries.empty?
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+ require 'connection_pool'
3
+ require_relative 'redis'
4
+
5
+ class RedisMemo::ConnectionPool
6
+ def initialize(**options)
7
+ @connection_pool = ::ConnectionPool.new(**options) do
8
+ # Construct a new client every time the block gets called
9
+ RedisMemo::Redis.new(RedisMemo::DefaultOptions.redis_config)
10
+ end
11
+ end
12
+
13
+ # Avoid method_missing when possible for better performance
14
+ %i(get mget mapped_mget set eval).each do |method_name|
15
+ define_method method_name do |*args, &blk|
16
+ @connection_pool.with do |redis|
17
+ redis.send(method_name, *args, &blk)
18
+ end
19
+ end
20
+ end
21
+
22
+ def method_missing(method_name, *args, &blk)
23
+ @connection_pool.with do |redis|
24
+ redis.send(method_name, *args, &blk)
25
+ end
26
+ end
27
+ end
@@ -9,14 +9,14 @@ class RedisMemo::Future
9
9
  ref,
10
10
  method_id,
11
11
  method_args,
12
- depends_on,
12
+ dependent_memos,
13
13
  cache_options,
14
14
  method_name_without_memo
15
15
  )
16
16
  @ref = ref
17
17
  @method_id = method_id
18
18
  @method_args = method_args
19
- @depends_on = depends_on
19
+ @dependent_memos = dependent_memos
20
20
  @cache_options = cache_options
21
21
  @method_name_without_memo = method_name_without_memo
22
22
  @method_cache_key = nil
@@ -28,7 +28,7 @@ class RedisMemo::Future
28
28
  end
29
29
 
30
30
  def context
31
- [@ref, @method_id, @method_args, @depends_on]
31
+ [@method_id, @method_args, @dependent_memos]
32
32
  end
33
33
 
34
34
  def method_cache_key
@@ -82,7 +82,11 @@ class RedisMemo::Memoizable
82
82
  if keys_to_fetch.empty?
83
83
  {}
84
84
  else
85
- RedisMemo::Cache.read_multi(*keys_to_fetch, raise_error: true)
85
+ RedisMemo::Cache.read_multi(
86
+ *keys_to_fetch,
87
+ raw: true,
88
+ raise_error: true,
89
+ )
86
90
  end
87
91
  memo_versions.merge!(cached_versions) unless cached_versions.empty?
88
92
 
@@ -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::ArgumentError,
55
- "Invalid Arel dependency. Query is not enabled for RedisMemo caching."
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
@@ -56,7 +56,10 @@ module RedisMemo::Memoizable::Invalidation
56
56
  # Fill an expected previous version so the later calculation results
57
57
  # based on this version can still be rolled out if this version
58
58
  # does not change
59
- previous_version ||= RedisMemo::Cache.read_multi(key)[key]
59
+ previous_version ||= RedisMemo::Cache.read_multi(
60
+ key,
61
+ raw: true,
62
+ )[key]
60
63
  end
61
64
 
62
65
  local_cache&.send(:[]=, key, version)
@@ -123,7 +126,8 @@ module RedisMemo::Memoizable::Invalidation
123
126
  task = @@invalidation_queue.pop
124
127
  begin
125
128
  bump_version(task)
126
- rescue SignalException, Redis::BaseConnectionError
129
+ rescue SignalException, Redis::BaseConnectionError,
130
+ ::ConnectionPool::TimeoutError
127
131
  retry_queue << task
128
132
  end
129
133
  end
@@ -15,6 +15,12 @@ module RedisMemo::MemoizeMethod
15
15
  define_method method_name_with_memo do |*args|
16
16
  return send(method_name_without_memo, *args) if RedisMemo.without_memo?
17
17
 
18
+ dependent_memos = nil
19
+ if depends_on
20
+ dependency = RedisMemo::MemoizeMethod.get_or_extract_dependencies(self, *args, &depends_on)
21
+ dependent_memos = dependency.memos
22
+ end
23
+
18
24
  future = RedisMemo::Future.new(
19
25
  self,
20
26
  case method_id
@@ -26,7 +32,7 @@ module RedisMemo::MemoizeMethod
26
32
  method_id.call(self, *args)
27
33
  end,
28
34
  args,
29
- depends_on,
35
+ dependent_memos,
30
36
  options,
31
37
  method_name_without_memo,
32
38
  )
@@ -37,6 +43,8 @@ module RedisMemo::MemoizeMethod
37
43
  end
38
44
 
39
45
  future.execute
46
+ rescue RedisMemo::WithoutMemoization
47
+ send(method_name_without_memo, *args)
40
48
  end
41
49
 
42
50
  alias_method method_name, method_name_with_memo
@@ -73,27 +81,98 @@ module RedisMemo::MemoizeMethod
73
81
 
74
82
  def self.get_or_extract_dependencies(ref, *method_args, &depends_on)
75
83
  if RedisMemo::Cache.local_dependency_cache
76
- RedisMemo::Cache.local_dependency_cache[ref] ||= {}
77
- RedisMemo::Cache.local_dependency_cache[ref][depends_on] ||= {}
78
- 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)
79
88
  else
80
89
  extract_dependencies(ref, *method_args, &depends_on)
81
90
  end
82
91
  end
83
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
+
84
166
  def self.method_cache_keys(future_contexts)
85
167
  memos = Array.new(future_contexts.size)
86
- future_contexts.each_with_index do |(ref, _, method_args, depends_on), i|
87
- if depends_on
88
- dependency = get_or_extract_dependencies(ref, *method_args, &depends_on)
89
- memos[i] = dependency.memos
90
- end
168
+ future_contexts.each_with_index do |(_, _, dependent_memos), i|
169
+ memos[i] = dependent_memos
91
170
  end
92
171
 
93
172
  j = 0
94
173
  memo_checksums = RedisMemo::Memoizable.checksums(memos.compact)
95
174
  method_cache_key_versions = Array.new(future_contexts.size)
96
- future_contexts.each_with_index do |(_, method_id, method_args, _), i|
175
+ future_contexts.each_with_index do |(method_id, method_args, _), i|
97
176
  if memos[i]
98
177
  method_cache_key_versions[i] = [method_id, memo_checksums[j]]
99
178
  j += 1
@@ -13,6 +13,8 @@ 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_#{self.table_name.upcase}"] == 'true'
17
+
16
18
  columns = raw_columns.map(&:to_sym).sort
17
19
 
18
20
  RedisMemo::MemoizeQuery.memoized_columns(self, editable_only: true) << columns if editable
@@ -102,8 +104,10 @@ if defined?(ActiveRecord)
102
104
  end
103
105
  end
104
106
 
105
- def self.invalidate(record)
106
- RedisMemo::Memoizable.invalidate(to_memos(record))
107
+ def self.invalidate(*records)
108
+ RedisMemo::Memoizable.invalidate(
109
+ records.map { |record| to_memos(record) }.flatten,
110
+ )
107
111
  end
108
112
 
109
113
  def self.to_memos(record)
@@ -111,19 +111,19 @@ 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,
126
- __redis_memo_memoize_query_memoize_query_binds__: binds.map(&:value_for_database),
119
+ __redis_memo_memoize_query_memoize_query_binds__: binds.map do |bind|
120
+ if bind.respond_to?(:value_for_database)
121
+ bind.value_for_database
122
+ else
123
+ # In activerecord >= 6, a bind could be an actual database value
124
+ bind
125
+ end
126
+ end
127
127
  )
128
128
  end
129
129
  end
@@ -210,7 +210,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
210
210
  bind_params = BindParams.new
211
211
 
212
212
  case node
213
- when Arel::Nodes::Equality, Arel::Nodes::In
213
+ when NodeHasFilterCondition
214
214
  attr_node = node.left
215
215
  return unless attr_node.is_a?(Arel::Attributes::Attribute)
216
216
 
@@ -247,7 +247,13 @@ class RedisMemo::MemoizeQuery::CachedSelect
247
247
  }
248
248
  when Arel::Nodes::Casted
249
249
  bind_params.params[binding_relation] << {
250
- right.attribute.name.to_sym => right.val,
250
+ right.attribute.name.to_sym =>
251
+ if right.respond_to?(:val)
252
+ right.val
253
+ else
254
+ # activerecord >= 6
255
+ right.value
256
+ end,
251
257
  }
252
258
  else
253
259
  bind_params = bind_params.union(extract_bind_params_recurse(right))
@@ -343,6 +349,23 @@ class RedisMemo::MemoizeQuery::CachedSelect
343
349
  enabled_models[table_node.try(:name)]
344
350
  end
345
351
 
352
+ class NodeHasFilterCondition
353
+ def self.===(node)
354
+ case node
355
+ when Arel::Nodes::Equality, Arel::Nodes::In
356
+ true
357
+ else
358
+ # In activerecord >= 6, a new arel node HomogeneousIn is introduced
359
+ if defined?(Arel::Nodes::HomogeneousIn) &&
360
+ node.is_a?(Arel::Nodes::HomogeneousIn)
361
+ true
362
+ else
363
+ false
364
+ end
365
+ end
366
+ end
367
+ end
368
+
346
369
  # Thread locals to exchange information between RedisMemo and ActiveRecord
347
370
  THREAD_KEY_AREL = :__redis_memo_memoize_query_cached_select_arel__
348
371
  THREAD_KEY_SUBSTITUTES = :__redis_memo_memoize_query_cached_select_substitues__
@@ -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
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
103
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,54 +192,42 @@ 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
- unique_by = options[:on_duplicate_key_update]
114
- if unique_by.is_a?(Hash)
115
- unique_by = unique_by[: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 unique_by
127
- RedisMemo::MemoizeQuery::Invalidation.send(
128
- :select_by_uniq_index,
129
- records,
130
- unique_by,
131
- )
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]
132
205
  else
133
- []
206
+ # Ignore duplicate rows
207
+ nil
134
208
  end
135
209
 
136
- result = send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
137
-
138
- records_to_invalidate += RedisMemo.without_memo do
139
- # Not all databases support "RETURNING", which is useful when
140
- # invaldating records after bulk creation
141
- model_class.where(model_class.primary_key => result.ids).to_a
210
+ if conflict_target && records.last.is_a?(Hash)
211
+ records.map! { |hash| model_class.new(hash) }
142
212
  end
143
213
 
144
- memos_to_invalidate = records_to_invalidate.map do |record|
145
- RedisMemo::MemoizeQuery.to_memos(record)
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)
146
220
  end
147
- RedisMemo::Memoizable.invalidate(memos_to_invalidate.flatten)
148
-
149
- result
150
221
  end
151
222
  end
152
223
  end
153
224
 
154
- def self.select_by_uniq_index(records, unique_by)
155
- model_class = records.first.class
225
+ def self.build_relation_by_conflict_target(model_class, records, conflict_target)
156
226
  or_chain = nil
157
227
 
158
228
  records.each do |record|
159
229
  conditions = {}
160
- unique_by.each do |column|
230
+ conflict_target.each do |column|
161
231
  conditions[column] = record.send(column)
162
232
  end
163
233
  if or_chain
@@ -167,6 +237,30 @@ class RedisMemo::MemoizeQuery::Invalidation
167
237
  end
168
238
  end
169
239
 
170
- RedisMemo.without_memo { or_chain.to_a }
240
+ or_chain
241
+ end
242
+
243
+ def self.select_by_new_ids(model_class, target_id)
244
+ RedisMemo::Tracer.trace(
245
+ 'redis_memo.memoize_query.invalidation',
246
+ "#{__method__}##{model_class.name}",
247
+ ) do
248
+ RedisMemo.without_memo do
249
+ model_class.where(
250
+ model_class.arel_table[model_class.primary_key].gt(target_id),
251
+ ).to_a
252
+ end
253
+ end
254
+ end
255
+
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
171
265
  end
172
266
  end
@@ -13,7 +13,7 @@ class RedisMemo::Options
13
13
  )
14
14
  @compress = compress.nil? ? true : compress
15
15
  @compress_threshold = compress_threshold || 1.kilobyte
16
- @redis = redis
16
+ @redis_config = redis
17
17
  @redis_client = nil
18
18
  @redis_error_handler = redis_error_handler
19
19
  @tracer = tracer
@@ -22,20 +22,18 @@ class RedisMemo::Options
22
22
  @expires_in = expires_in
23
23
  end
24
24
 
25
- def redis(&blk)
26
- if blk.nil?
27
- return @redis_client if @redis_client.is_a?(RedisMemo::Redis)
25
+ def redis
26
+ @redis_client ||= RedisMemo::Redis.new(redis_config)
27
+ end
28
28
 
29
- if @redis.respond_to?(:call)
30
- @redis_client = RedisMemo::Redis.new(@redis.call)
31
- elsif @redis
32
- @redis_client = RedisMemo::Redis.new(@redis)
33
- else
34
- @redis_client = RedisMemo::Redis.new
35
- end
36
- else
37
- @redis = blk
38
- end
29
+ def redis_config
30
+ @redis_config || {}
31
+ end
32
+
33
+ def redis=(config)
34
+ @redis_config = config
35
+ @redis_client = nil
36
+ redis
39
37
  end
40
38
 
41
39
  def tracer(&blk)
@@ -74,15 +72,15 @@ class RedisMemo::Options
74
72
  end
75
73
 
76
74
  attr_accessor :async
75
+ attr_accessor :cache_out_of_date_handler
76
+ attr_accessor :cache_validation_sampler
77
77
  attr_accessor :compress
78
78
  attr_accessor :compress_threshold
79
- attr_accessor :redis_error_handler
79
+ attr_accessor :connection_pool
80
80
  attr_accessor :expires_in
81
- attr_accessor :cache_validation_sampler
82
- attr_accessor :cache_out_of_date_handler
81
+ attr_accessor :redis_error_handler
83
82
 
84
83
  attr_writer :global_cache_key_version
85
- attr_writer :redis
86
84
  attr_writer :tracer
87
85
  attr_writer :logger
88
86
  end
@@ -31,7 +31,8 @@ class RedisMemo::Redis < Redis::Distributed
31
31
  end
32
32
 
33
33
  class WithReplicas < ::Redis
34
- def initialize(options)
34
+ def initialize(orig_options)
35
+ options = orig_options.dup
35
36
  primary_option = options.shift
36
37
  @replicas = options.map do |option|
37
38
  option[:logger] ||= RedisMemo::DefaultOptions.logger
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.3
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chan Zuckerberg Initiative
@@ -14,42 +14,56 @@ dependencies:
14
14
  name: activesupport
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '5.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: redis
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '4'
33
+ version: 4.0.1
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 4.0.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: connection_pool
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: 2.2.3
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
39
53
  - !ruby/object:Gem::Version
40
- version: '4'
54
+ version: 2.2.3
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: activerecord
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
- - - "~>"
59
+ - - ">="
46
60
  - !ruby/object:Gem::Version
47
61
  version: '5.2'
48
62
  type: :development
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
- - - "~>"
66
+ - - ">="
53
67
  - !ruby/object:Gem::Version
54
68
  version: '5.2'
55
69
  - !ruby/object:Gem::Dependency
@@ -161,6 +175,7 @@ files:
161
175
  - lib/redis_memo/after_commit.rb
162
176
  - lib/redis_memo/batch.rb
163
177
  - lib/redis_memo/cache.rb
178
+ - lib/redis_memo/connection_pool.rb
164
179
  - lib/redis_memo/future.rb
165
180
  - lib/redis_memo/memoizable.rb
166
181
  - lib/redis_memo/memoizable/dependency.rb
@@ -193,9 +208,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
193
208
  version: 2.5.0
194
209
  required_rubygems_version: !ruby/object:Gem::Requirement
195
210
  requirements:
196
- - - ">"
211
+ - - ">="
197
212
  - !ruby/object:Gem::Version
198
- version: 1.3.1
213
+ version: '0'
199
214
  requirements: []
200
215
  rubygems_version: 3.0.8
201
216
  signing_key: