redis-memo 0.0.0.beta.3 → 0.1.1

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: 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: