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

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