redis-memo 0.1.1 → 1.1.0

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  class RedisMemo::MemoizeQuery::CachedSelect
4
4
  module ConnectionAdapter
5
- def cacheable_query(*args)
5
+ ruby2_keywords def cacheable_query(*args)
6
6
  query, binds = super(*args)
7
7
 
8
8
  # Persist the arel object to StatementCache#execute
@@ -11,24 +11,33 @@ class RedisMemo::MemoizeQuery::CachedSelect
11
11
  [query, binds]
12
12
  end
13
13
 
14
- def exec_query(*args)
14
+ ruby2_keywords def exec_query(*args)
15
15
  # An Arel AST in Thread local is set prior to supported query methods
16
- if !RedisMemo.without_memo? &&
16
+ if !RedisMemo.without_memoization? &&
17
17
  RedisMemo::MemoizeQuery::CachedSelect.extract_bind_params(args[0])
18
+
19
+ time_start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
20
+ ret = super(*args)
21
+ time_end = Process.clock_gettime(Process::CLOCK_MONOTONIC)
22
+
18
23
  # [Reids $model Load] $sql $binds
19
- RedisMemo::DefaultOptions.logger&.info(
20
- "[Redis] \u001b[36;1m#{args[1]} \u001b[34;1m#{args[0]}\u001b[0m #{
21
- args[2].map { |bind| [bind.name, bind.value_for_database]}
22
- }"
24
+ RedisMemo::DefaultOptions.logger&.debug(
25
+ "[Redis] \u001b[36;1m#{
26
+ args[1] || 'SQL' # model name
27
+ } (#{format('%.1f', (time_end - time_start) * 1000.0)}ms) \u001b[34;1m#{
28
+ args[0] # sql
29
+ }\u001b[0m #{
30
+ args[2].map { |bind| [bind.name, bind.value_for_database] } # binds
31
+ }",
23
32
  )
24
33
 
25
- super(*args)
34
+ ret
26
35
  else
27
- RedisMemo.without_memo { super(*args) }
36
+ RedisMemo.without_memoization { super(*args) }
28
37
  end
29
38
  end
30
39
 
31
- def select_all(*args)
40
+ ruby2_keywords def select_all(*args)
32
41
  if args[0].is_a?(Arel::SelectManager)
33
42
  RedisMemo::MemoizeQuery::CachedSelect.current_query = args[0]
34
43
  end
@@ -17,28 +17,29 @@ class RedisMemo::MemoizeQuery::Invalidation
17
17
  @redis_memo_class_memoizable ||= RedisMemo::MemoizeQuery.create_memo(self)
18
18
  end
19
19
 
20
- %i(delete decrement! increment!).each do |method_name|
20
+ %i[delete decrement! increment!].each do |method_name|
21
21
  alias_method :"without_redis_memo_invalidation_#{method_name}", method_name
22
22
 
23
23
  define_method method_name do |*args|
24
- result = send(:"without_redis_memo_invalidation_#{method_name}", *args)
24
+ result = __send__(:"without_redis_memo_invalidation_#{method_name}", *args)
25
25
 
26
26
  RedisMemo::MemoizeQuery.invalidate(self)
27
27
 
28
28
  result
29
29
  end
30
+ ruby2_keywords method_name
30
31
  end
31
32
  end
32
33
 
33
34
  # Methods that won't trigger model callbacks
34
35
  # https://guides.rubyonrails.org/active_record_callbacks.html#skipping-callbacks
35
- %i(
36
+ %i[
36
37
  decrement_counter
37
38
  delete_all delete_by
38
39
  increment_counter
39
40
  touch_all
40
41
  update_column update_columns update_all update_counters
41
- ).each do |method_name|
42
+ ].each do |method_name|
42
43
  # Example: Model.update_all
43
44
  rewrite_default_method(
44
45
  model_class,
@@ -56,27 +57,27 @@ class RedisMemo::MemoizeQuery::Invalidation
56
57
  )
57
58
  end
58
59
 
59
- %i(
60
+ %i[
60
61
  insert insert! insert_all insert_all!
61
- ).each do |method_name|
62
+ ].each do |method_name|
62
63
  rewrite_insert_method(
63
64
  model_class,
64
65
  method_name,
65
66
  )
66
67
  end
67
68
 
68
- %i(
69
+ %i[
69
70
  upsert upsert_all
70
- ).each do |method_name|
71
+ ].each do |method_name|
71
72
  rewrite_upsert_method(
72
73
  model_class,
73
74
  method_name,
74
75
  )
75
76
  end
76
77
 
77
- %i(
78
+ %i[
78
79
  import import!
79
- ).each do |method_name|
80
+ ].each do |method_name|
80
81
  rewrite_import_method(
81
82
  model_class,
82
83
  method_name,
@@ -116,8 +117,6 @@ class RedisMemo::MemoizeQuery::Invalidation
116
117
  result
117
118
  end
118
119
 
119
- private
120
-
121
120
  #
122
121
  # There’s no good way to perform fine-grind cache invalidation when
123
122
  # operations are bulk update operations such as update_all, and delete_all
@@ -127,17 +126,18 @@ class RedisMemo::MemoizeQuery::Invalidation
127
126
  #
128
127
  def self.rewrite_default_method(model_class, klass, method_name, class_method:)
129
128
  methods = class_method ? :methods : :instance_methods
130
- return unless klass.send(methods).include?(method_name)
129
+ return unless klass.__send__(methods).include?(method_name)
131
130
 
132
131
  klass = klass.singleton_class if class_method
133
132
  klass.class_eval do
134
133
  alias_method :"#{method_name}_without_redis_memo_invalidation", method_name
135
134
 
136
135
  define_method method_name do |*args|
137
- result = send(:"#{method_name}_without_redis_memo_invalidation", *args)
136
+ result = __send__(:"#{method_name}_without_redis_memo_invalidation", *args)
138
137
  RedisMemo::MemoizeQuery.invalidate_all(model_class)
139
138
  result
140
139
  end
140
+ ruby2_keywords method_name
141
141
  end
142
142
  end
143
143
 
@@ -149,9 +149,10 @@ class RedisMemo::MemoizeQuery::Invalidation
149
149
 
150
150
  define_method method_name do |*args, &blk|
151
151
  RedisMemo::MemoizeQuery::Invalidation.invalidate_new_records(model_class) do
152
- send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
152
+ __send__(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
153
153
  end
154
154
  end
155
+ ruby2_keywords method_name
155
156
  end
156
157
  end
157
158
 
@@ -169,7 +170,7 @@ class RedisMemo::MemoizeQuery::Invalidation
169
170
  # HEAD (6.1.3)
170
171
  conflict_target: nil,
171
172
  ) do
172
- send(
173
+ __send__(
173
174
  :"#{method_name}_without_redis_memo_invalidation",
174
175
  attributes,
175
176
  unique_by: unique_by,
@@ -216,9 +217,10 @@ class RedisMemo::MemoizeQuery::Invalidation
216
217
  records: records,
217
218
  conflict_target: conflict_target,
218
219
  ) do
219
- send(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
220
+ __send__(:"#{method_name}_without_redis_memo_invalidation", *args, &blk)
220
221
  end
221
222
  end
223
+ ruby2_keywords method_name
222
224
  end
223
225
  end
224
226
 
@@ -228,7 +230,7 @@ class RedisMemo::MemoizeQuery::Invalidation
228
230
  records.each do |record|
229
231
  conditions = {}
230
232
  conflict_target.each do |column|
231
- conditions[column] = record.send(column)
233
+ conditions[column] = record.__send__(column)
232
234
  end
233
235
  if or_chain
234
236
  or_chain = or_chain.or(model_class.where(conditions))
@@ -245,7 +247,7 @@ class RedisMemo::MemoizeQuery::Invalidation
245
247
  'redis_memo.memoize_query.invalidation',
246
248
  "#{__method__}##{model_class.name}",
247
249
  ) do
248
- RedisMemo.without_memo do
250
+ RedisMemo.without_memoization do
249
251
  model_class.where(
250
252
  model_class.arel_table[model_class.primary_key].gt(target_id),
251
253
  ).to_a
@@ -260,7 +262,7 @@ class RedisMemo::MemoizeQuery::Invalidation
260
262
  'redis_memo.memoize_query.invalidation',
261
263
  "#{__method__}##{model_class.name}",
262
264
  ) do
263
- RedisMemo.without_memo { relation.reload }
265
+ RedisMemo.without_memoization { relation.reload }
264
266
  end
265
267
  end
266
268
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'memoize_method'
3
4
 
4
5
  module RedisMemo::MemoizeQuery
@@ -9,7 +9,9 @@ class RedisMemo::Middleware
9
9
  result = nil
10
10
 
11
11
  RedisMemo::Cache.with_local_cache do
12
- result = @app.call(env)
12
+ RedisMemo.with_max_connection_attempts(RedisMemo::DefaultOptions.max_connection_attempts) do
13
+ result = @app.call(env)
14
+ end
13
15
  end
14
16
  RedisMemo::Memoizable::Invalidation.drain_invalidation_queue
15
17
 
@@ -1,5 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ ##
4
+ # This class allows users to configure various RedisMemo options. Options can be set in
5
+ # your initializer +config/initializers/redis_memo.rb+
6
+ # RedisMemo.configure do |config|
7
+ # config.expires_in = 3.hours
8
+ # config.global_cache_key_version = SecureRandom.uuid
9
+ # end
10
+ #
3
11
  class RedisMemo::Options
4
12
  def initialize(
5
13
  async: nil,
@@ -9,33 +17,70 @@ class RedisMemo::Options
9
17
  redis_error_handler: nil,
10
18
  tracer: nil,
11
19
  global_cache_key_version: nil,
12
- expires_in: nil
20
+ expires_in: nil,
21
+ max_connection_attempts: nil,
22
+ max_query_dependency_size: 5000,
23
+ disable_all: false,
24
+ disable_cached_select: false,
25
+ disabled_models: Set.new
13
26
  )
27
+ @async = async
14
28
  @compress = compress.nil? ? true : compress
15
29
  @compress_threshold = compress_threshold || 1.kilobyte
16
30
  @redis_config = redis
17
- @redis_client = nil
31
+ @redis = nil
18
32
  @redis_error_handler = redis_error_handler
19
33
  @tracer = tracer
20
34
  @logger = logger
21
35
  @global_cache_key_version = global_cache_key_version
22
36
  @expires_in = expires_in
37
+ @max_connection_attempts = ENV['REDIS_MEMO_MAX_ATTEMPTS_PER_REQUEST']&.to_i || max_connection_attempts
38
+ @max_query_dependency_size = ENV['REDIS_MEMO_MAX_QUERY_DEPENDENCY_SIZE']&.to_i || max_query_dependency_size
39
+ @disable_all = ENV['REDIS_MEMO_DISABLE_ALL'] == 'true' || disable_all
40
+ @disable_cached_select = ENV['REDIS_MEMO_DISABLE_CACHED_SELECT'] == 'true' || disable_cached_select
41
+ @disabled_models = disabled_models
23
42
  end
24
43
 
44
+ # Retrieves the redis client, initializing it if it does not exist yet.
25
45
  def redis
26
- @redis_client ||= RedisMemo::Redis.new(redis_config)
46
+ @redis ||= RedisMemo::Redis.new(redis_config)
27
47
  end
28
48
 
49
+ # Retrieves the config values used to initialize the Redis client.
29
50
  def redis_config
30
51
  @redis_config || {}
31
52
  end
32
53
 
54
+ # Set configuration values to pass to the Redis client. If multiple configurations are passed
55
+ # to this method, we assume that the first config corresponds to the primary node, and subsequent
56
+ # configurations correspond to replica nodes.
57
+ #
58
+ # For example, if your urls are specified as <tt><url>,<url>...;<url>,...;...,</tt> where <tt>;</tt> delimits
59
+ # different clusters and <tt>,</tt> delimits primary and read replicas, then in your configuration:
60
+ #
61
+ # RedisMemo.configure do |config|
62
+ # config.redis = redis_urls.split(';').map do |urls|
63
+ # urls.split(',').map do |url|
64
+ # {
65
+ # url: url,
66
+ # # All timeout values are specified in seconds
67
+ # connect_timeout: ENV['REDIS_MEMO_CONNECT_TIMEOUT']&.to_f || 0.2,
68
+ # read_timeout: ENV['REDIS_MEMO_READ_TIMEOUT']&.to_f || 0.5,
69
+ # write_timeout: ENV['REDIS_MEMO_WRITE_TIMEOUT']&.to_f || 0.5,
70
+ # reconnect_attempts: ENV['REDIS_MEMO_RECONNECT_ATTEMPTS']&.to_i || 0
71
+ # }
72
+ # end
73
+ # end
74
+ # end
75
+ #
33
76
  def redis=(config)
34
77
  @redis_config = config
35
- @redis_client = nil
78
+ @redis = nil
36
79
  redis
37
80
  end
38
81
 
82
+ # Sets the tracer object. Allows the tracer to be dynamically determined at
83
+ # runtime if a blk is given.
39
84
  def tracer(&blk)
40
85
  if blk.nil?
41
86
  return @tracer if @tracer.respond_to?(:trace)
@@ -46,6 +91,8 @@ class RedisMemo::Options
46
91
  end
47
92
  end
48
93
 
94
+ # Sets the logger object in RedisMemo. Allows the logger to be dynamically
95
+ # determined at runtime if a blk is given.
49
96
  def logger(&blk)
50
97
  if blk.nil?
51
98
  return @logger if @logger.respond_to?(:warn)
@@ -56,6 +103,8 @@ class RedisMemo::Options
56
103
  end
57
104
  end
58
105
 
106
+ # Sets the global cache key version. Allows the logger to be dynamically
107
+ # determined at runtime if a blk is given.
59
108
  def global_cache_key_version(&blk)
60
109
  # this method takes a block to be consistent with the inline memo_method
61
110
  # API
@@ -71,16 +120,73 @@ class RedisMemo::Options
71
120
  end
72
121
  end
73
122
 
123
+ # Disables the model for caching and invalidation
124
+ def disable_model(model)
125
+ @disabled_models << model
126
+ end
127
+
128
+ # Checks if a model is disabled for redis memo caching
129
+ def model_disabled_for_caching?(model)
130
+ ENV["REDIS_MEMO_DISABLE_#{model.table_name.upcase}"] == 'true' || @disabled_models.include?(model)
131
+ end
132
+
133
+ # A handler used to asynchronously perform cache writes and invalidations. If no value is provided,
134
+ # RedisMemo will perform these operations synchronously.
74
135
  attr_accessor :async
136
+
137
+ # Specify the global sampled percentage of the chance to call the cache validation, a value between 0 to 100, when the value
138
+ # is 100, it will call the handler every time the cached result does not match the uncached result
139
+ # You can also specify inline cache validation sample percentage by memoize_method :method, cache_validation_sample_percentage: #{value}
140
+ attr_accessor :cache_validation_sample_percentage
141
+
142
+ # Handler called when the cached result does not match the uncached result (sampled at the
143
+ # `cache_validation_sample_percentage`). This might indicate that invalidation is happening too slowly or
144
+ # that there are incorrect dependencies specified on a cached method.
75
145
  attr_accessor :cache_out_of_date_handler
76
- attr_accessor :cache_validation_sampler
146
+
147
+ # Passed along to the Rails {RedisCacheStore}[https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html], determines whether or not to compress entries before storing
148
+ # them. default: `true`
77
149
  attr_accessor :compress
150
+
151
+ # Passed along to the Rails {RedisCacheStore}[https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html], the size threshold for which to compress cached entries.
152
+ # default: 1.kilobyte
78
153
  attr_accessor :compress_threshold
154
+
155
+ # Configuration values for connecting to RedisMemo using a connection pool. It's recommended to use a
156
+ # connection pool in multi-threaded applications, or when an async handler is set.
79
157
  attr_accessor :connection_pool
158
+
159
+ # Passed along to the Rails {RedisCacheStore}[https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html], sets the TTL on cache entries in Redis.
80
160
  attr_accessor :expires_in
161
+
162
+ # The max number of failed connection attempts RedisMemo will make for a single request before bypassing
163
+ # the caching layer. This helps make RedisMemo resilient to errors and performance issues when there's
164
+ # an issue with the Redis cluster itself.
165
+ attr_accessor :max_connection_attempts
166
+
167
+ # Only cache a SQL query when the max number of dependency is smaller or equal to this number. Configurable via an ENV var REDIS_MEMO_MAX_QUERY_DEPENDENCY_SIZE. Default at 5000.
168
+ attr_accessor :max_query_dependency_size
169
+
170
+ # Passed along to the Rails {RedisCacheStore}[https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html], the error handler called for Redis related errors.
81
171
  attr_accessor :redis_error_handler
82
172
 
173
+ # A global kill switch to disable all RedisMemo operations.
174
+ attr_accessor :disable_all
175
+
176
+ # A kill switch to disable RedisMemo caching on database queries. This does not disable the invalidation
177
+ # after_save hooks that are installed on memoized models.
178
+ attr_accessor :disable_cached_select
179
+
180
+ # A kill switch to set the list of models to disable caching and invalidation after_save hooks on.
181
+ attr_accessor :disabled_models
182
+
183
+ # A global cache key version prepended to each cached entry. For example, the commit hash of the current
184
+ # version deployed to your application.
83
185
  attr_writer :global_cache_key_version
186
+
187
+ # Object used to trace RedisMemo operations to collect latency and error metrics, e.g. `Datadog.tracer`
84
188
  attr_writer :tracer
189
+
190
+ # Object used to log RedisMemo operations.
85
191
  attr_writer :logger
86
192
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class RedisMemo::Railtie < Rails::Railtie
4
+ initializer 'request_store.insert_middleware' do |app|
5
+ if ActionDispatch.const_defined? :RequestId
6
+ app.config.middleware.insert_after ActionDispatch::RequestId, RedisMemo::Middleware
7
+ else
8
+ app.config.middleware.insert_after Rack::MethodOverride, RedisMemo::Middleware
9
+ end
10
+ end
11
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'redis'
3
4
  require 'redis/distributed'
4
5
 
@@ -7,19 +8,19 @@ require_relative 'options'
7
8
  # Redis::Distributed does not support reading from multiple read replicas. This
8
9
  # class adds this functionality
9
10
  class RedisMemo::Redis < Redis::Distributed
10
- def initialize(options={})
11
+ def initialize(
12
+ options = {} # rubocop: disable Style/OptionHash
13
+ )
11
14
  clients =
12
15
  if options.is_a?(Array)
13
16
  options.map do |option|
14
17
  if option.is_a?(Array)
15
18
  RedisMemo::Redis::WithReplicas.new(option)
16
19
  else
17
- option[:logger] ||= RedisMemo::DefaultOptions.logger
18
20
  ::Redis.new(option)
19
21
  end
20
22
  end
21
23
  else
22
- options[:logger] ||= RedisMemo::DefaultOptions.logger
23
24
  [::Redis.new(options)]
24
25
  end
25
26
 
@@ -30,16 +31,25 @@ class RedisMemo::Redis < Redis::Distributed
30
31
  super([], ring: hash_ring)
31
32
  end
32
33
 
34
+ def run_script(script_content, script_sha, *args)
35
+ begin
36
+ return evalsha(script_sha, *args) if script_sha
37
+ rescue Redis::CommandError => error
38
+ if error.message != 'NOSCRIPT No matching script. Please use EVAL.'
39
+ raise error
40
+ end
41
+ end
42
+ eval(script_content, *args) # rubocop: disable Security/Eval
43
+ end
44
+
33
45
  class WithReplicas < ::Redis
34
46
  def initialize(orig_options)
35
47
  options = orig_options.dup
36
48
  primary_option = options.shift
37
49
  @replicas = options.map do |option|
38
- option[:logger] ||= RedisMemo::DefaultOptions.logger
39
50
  ::Redis.new(option)
40
51
  end
41
52
 
42
- primary_option[:logger] ||= RedisMemo::DefaultOptions.logger
43
53
  super(primary_option)
44
54
  end
45
55