redis-memo 0.1.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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