redis-memo 0.0.0.beta.3 → 0.0.0.beta.4

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: 72bcf0679b811825222a6b38de0fcdadf4546640d2f61b18ebc827e37137dc41
4
+ data.tar.gz: 7a55c7bbd084a24afe085e85bc5cd5c486cf240094b64ca09d48e9e0ce126384
5
5
  SHA512:
6
- metadata.gz: a8f573eb77832f3dfb0600ab607c8d60090969082c86a9403a98b784899931da60d66f1b761bf863fd4007492ff47dee2d7880ec2f10cf5143fce33bc791684b
7
- data.tar.gz: 4f9e44c13d2b4918f69f143f78eb4d80ad306b43abb650c03c481d727a88938f70c3ef89854e5c67afe92cc656df82ff44df84bb07878904bcdfbb2b86c15275
6
+ metadata.gz: 5a3af887512e4b6571829fcfda719e43abb917eb4773407bd27788b0b671a42263ec31c2f6286f0a1279a354851c8c8cb48eb9701cba59863e500ca47419080e
7
+ data.tar.gz: feeb72b177ec07953b839e6e0759cbdf42dfd4eddd19f89599e721589462e849e7685a659b03128504d670764223f1805402cb3ee8a96411b834f229ba837d99
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
 
@@ -51,8 +51,8 @@ class RedisMemo::Memoizable::Dependency
51
51
  RedisMemo::MemoizeQuery::CachedSelect.current_query = relation.arel
52
52
  is_query_cached = RedisMemo::MemoizeQuery::CachedSelect.extract_bind_params(query)
53
53
  raise(
54
- RedisMemo::ArgumentError,
55
- "Invalid Arel dependency. Query is not enabled for RedisMemo caching."
54
+ RedisMemo::WithoutMemoization,
55
+ "Arel query is not cached using RedisMemo."
56
56
  ) unless is_query_cached
57
57
  extracted_dependency = connection.dependency_of(:exec_query, query, nil, binds)
58
58
  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
@@ -83,17 +91,14 @@ module RedisMemo::MemoizeMethod
83
91
 
84
92
  def self.method_cache_keys(future_contexts)
85
93
  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
94
+ future_contexts.each_with_index do |(_, _, dependent_memos), i|
95
+ memos[i] = dependent_memos
91
96
  end
92
97
 
93
98
  j = 0
94
99
  memo_checksums = RedisMemo::Memoizable.checksums(memos.compact)
95
100
  method_cache_key_versions = Array.new(future_contexts.size)
96
- future_contexts.each_with_index do |(_, method_id, method_args, _), i|
101
+ future_contexts.each_with_index do |(method_id, method_args, _), i|
97
102
  if memos[i]
98
103
  method_cache_key_versions[i] = [method_id, memo_checksums[j]]
99
104
  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)
@@ -123,7 +123,14 @@ class RedisMemo::MemoizeQuery::CachedSelect
123
123
 
124
124
  depends_on RedisMemo::Memoizable.new(
125
125
  __redis_memo_memoize_query_memoize_query_sql__: sql,
126
- __redis_memo_memoize_query_memoize_query_binds__: binds.map(&:value_for_database),
126
+ __redis_memo_memoize_query_memoize_query_binds__: binds.map do |bind|
127
+ if bind.respond_to?(:value_for_database)
128
+ bind.value_for_database
129
+ else
130
+ # In activerecord >= 6, a bind could be an actual database value
131
+ bind
132
+ end
133
+ end
127
134
  )
128
135
  end
129
136
  end
@@ -210,7 +217,7 @@ class RedisMemo::MemoizeQuery::CachedSelect
210
217
  bind_params = BindParams.new
211
218
 
212
219
  case node
213
- when Arel::Nodes::Equality, Arel::Nodes::In
220
+ when NodeHasFilterCondition
214
221
  attr_node = node.left
215
222
  return unless attr_node.is_a?(Arel::Attributes::Attribute)
216
223
 
@@ -247,7 +254,13 @@ class RedisMemo::MemoizeQuery::CachedSelect
247
254
  }
248
255
  when Arel::Nodes::Casted
249
256
  bind_params.params[binding_relation] << {
250
- right.attribute.name.to_sym => right.val,
257
+ right.attribute.name.to_sym =>
258
+ if right.respond_to?(:val)
259
+ right.val
260
+ else
261
+ # activerecord >= 6
262
+ right.value
263
+ end,
251
264
  }
252
265
  else
253
266
  bind_params = bind_params.union(extract_bind_params_recurse(right))
@@ -343,6 +356,23 @@ class RedisMemo::MemoizeQuery::CachedSelect
343
356
  enabled_models[table_node.try(:name)]
344
357
  end
345
358
 
359
+ class NodeHasFilterCondition
360
+ def self.===(node)
361
+ case node
362
+ when Arel::Nodes::Equality, Arel::Nodes::In
363
+ true
364
+ else
365
+ # In activerecord >= 6, a new arel node HomogeneousIn is introduced
366
+ if defined?(Arel::Nodes::HomogeneousIn) &&
367
+ node.is_a?(Arel::Nodes::HomogeneousIn)
368
+ true
369
+ else
370
+ false
371
+ end
372
+ end
373
+ end
374
+ end
375
+
346
376
  # Thread locals to exchange information between RedisMemo and ActiveRecord
347
377
  THREAD_KEY_AREL = :__redis_memo_memoize_query_cached_select_arel__
348
378
  THREAD_KEY_SUBSTITUTES = :__redis_memo_memoize_query_cached_select_substitues__
@@ -110,9 +110,9 @@ class RedisMemo::MemoizeQuery::Invalidation
110
110
  define_method method_name do |*args, &blk|
111
111
  options = args.last.is_a?(Hash) ? args.last : {}
112
112
  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]
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
116
  end
117
117
 
118
118
  if records.last.is_a?(Hash)
@@ -123,11 +123,11 @@ class RedisMemo::MemoizeQuery::Invalidation
123
123
  # - default values filled by the database
124
124
  # - updates on conflict conditions
125
125
  records_to_invalidate =
126
- if unique_by
126
+ if columns_to_update
127
127
  RedisMemo::MemoizeQuery::Invalidation.send(
128
- :select_by_uniq_index,
128
+ :select_by_columns,
129
129
  records,
130
- unique_by,
130
+ columns_to_update,
131
131
  )
132
132
  else
133
133
  []
@@ -135,38 +135,77 @@ class RedisMemo::MemoizeQuery::Invalidation
135
135
 
136
136
  result = send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
137
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
138
+ # Offload the records to invalidate while selecting the next set of
139
+ # records to invalidate
140
+ case records_to_invalidate
141
+ when Array
142
+ RedisMemo::MemoizeQuery.invalidate(*records_to_invalidate) unless records_to_invalidate.empty?
143
+
144
+ RedisMemo::MemoizeQuery.invalidate(*RedisMemo::MemoizeQuery::Invalidation.send(
145
+ :select_by_id,
146
+ model_class,
147
+ # Not all databases support "RETURNING", which is useful when
148
+ # invaldating records after bulk creation
149
+ result.ids,
150
+ ))
151
+ else
152
+ RedisMemo::MemoizeQuery.invalidate_all(model_class)
142
153
  end
143
154
 
144
- memos_to_invalidate = records_to_invalidate.map do |record|
145
- RedisMemo::MemoizeQuery.to_memos(record)
146
- end
147
- RedisMemo::Memoizable.invalidate(memos_to_invalidate.flatten)
148
-
149
155
  result
150
156
  end
151
157
  end
152
158
  end
153
159
 
154
- def self.select_by_uniq_index(records, unique_by)
160
+ def self.select_by_columns(records, columns_to_update)
155
161
  model_class = records.first.class
156
162
  or_chain = nil
157
-
158
- records.each do |record|
159
- conditions = {}
160
- unique_by.each do |column|
161
- conditions[column] = record.send(column)
163
+ columns_to_select = columns_to_update & RedisMemo::MemoizeQuery
164
+ .memoized_columns(model_class)
165
+ .to_a.flatten.uniq
166
+
167
+ # Nothing to invalidate here
168
+ return [] if columns_to_select.empty?
169
+
170
+ RedisMemo::Tracer.trace(
171
+ 'redis_memo.memoize_query.invalidation',
172
+ "#{__method__}##{model_class.name}",
173
+ ) do
174
+ records.each do |record|
175
+ conditions = {}
176
+ columns_to_select.each do |column|
177
+ conditions[column] = record.send(column)
178
+ end
179
+ if or_chain
180
+ or_chain = or_chain.or(model_class.where(conditions))
181
+ else
182
+ or_chain = model_class.where(conditions)
183
+ end
162
184
  end
163
- if or_chain
164
- or_chain = or_chain.or(model_class.where(conditions))
185
+
186
+ record_count = RedisMemo.without_memo { or_chain.count }
187
+ if record_count > bulk_operations_invalidation_limit
188
+ nil
165
189
  else
166
- or_chain = model_class.where(conditions)
190
+ RedisMemo.without_memo { or_chain.to_a }
167
191
  end
168
192
  end
193
+ end
194
+
195
+ def self.select_by_id(model_class, ids)
196
+ RedisMemo::Tracer.trace(
197
+ 'redis_memo.memoize_query.invalidation',
198
+ "#{__method__}##{model_class.name}",
199
+ ) do
200
+ RedisMemo.without_memo do
201
+ model_class.where(model_class.primary_key => ids).to_a
202
+ end
203
+ end
204
+ end
169
205
 
170
- RedisMemo.without_memo { or_chain.to_a }
206
+ def self.bulk_operations_invalidation_limit
207
+ ENV['REDIS_MEMO_BULK_OPERATIONS_INVALIDATION_LIMIT']&.to_i ||
208
+ RedisMemo::DefaultOptions.bulk_operations_invalidation_limit ||
209
+ 10000
171
210
  end
172
211
  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,16 @@ class RedisMemo::Options
74
72
  end
75
73
 
76
74
  attr_accessor :async
75
+ attr_accessor :bulk_operations_invalidation_limit
76
+ attr_accessor :cache_out_of_date_handler
77
+ attr_accessor :cache_validation_sampler
77
78
  attr_accessor :compress
78
79
  attr_accessor :compress_threshold
79
- attr_accessor :redis_error_handler
80
+ attr_accessor :connection_pool
80
81
  attr_accessor :expires_in
81
- attr_accessor :cache_validation_sampler
82
- attr_accessor :cache_out_of_date_handler
82
+ attr_accessor :redis_error_handler
83
83
 
84
84
  attr_writer :global_cache_key_version
85
- attr_writer :redis
86
85
  attr_writer :tracer
87
86
  attr_writer :logger
88
87
  end
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.0.0.beta.4
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
39
  - !ruby/object:Gem::Version
40
- version: '4'
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
+ - - ">="
53
+ - !ruby/object:Gem::Version
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