redis-memo 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,68 @@ 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
+ disable_all: false,
23
+ disable_cached_select: false,
24
+ disabled_models: Set.new
13
25
  )
26
+ @async = async
14
27
  @compress = compress.nil? ? true : compress
15
28
  @compress_threshold = compress_threshold || 1.kilobyte
16
29
  @redis_config = redis
17
- @redis_client = nil
30
+ @redis = nil
18
31
  @redis_error_handler = redis_error_handler
19
32
  @tracer = tracer
20
33
  @logger = logger
21
34
  @global_cache_key_version = global_cache_key_version
22
35
  @expires_in = expires_in
36
+ @max_connection_attempts = ENV['REDIS_MEMO_MAX_ATTEMPTS_PER_REQUEST']&.to_i || max_connection_attempts
37
+ @disable_all = ENV['REDIS_MEMO_DISABLE_ALL'] == 'true' || disable_all
38
+ @disable_cached_select = ENV['REDIS_MEMO_DISABLE_CACHED_SELECT'] == 'true' || disable_cached_select
39
+ @disabled_models = disabled_models
23
40
  end
24
41
 
42
+ # Retrieves the redis client, initializing it if it does not exist yet.
25
43
  def redis
26
- @redis_client ||= RedisMemo::Redis.new(redis_config)
44
+ @redis ||= RedisMemo::Redis.new(redis_config)
27
45
  end
28
46
 
47
+ # Retrieves the config values used to initialize the Redis client.
29
48
  def redis_config
30
49
  @redis_config || {}
31
50
  end
32
51
 
52
+ # Set configuration values to pass to the Redis client. If multiple configurations are passed
53
+ # to this method, we assume that the first config corresponds to the primary node, and subsequent
54
+ # configurations correspond to replica nodes.
55
+ #
56
+ # For example, if your urls are specified as <tt><url>,<url>...;<url>,...;...,</tt> where <tt>;</tt> delimits
57
+ # different clusters and <tt>,</tt> delimits primary and read replicas, then in your configuration:
58
+ #
59
+ # RedisMemo.configure do |config|
60
+ # config.redis = redis_urls.split(';').map do |urls|
61
+ # urls.split(',').map do |url|
62
+ # {
63
+ # url: url,
64
+ # # All timeout values are specified in seconds
65
+ # connect_timeout: ENV['REDIS_MEMO_CONNECT_TIMEOUT']&.to_f || 0.2,
66
+ # read_timeout: ENV['REDIS_MEMO_READ_TIMEOUT']&.to_f || 0.5,
67
+ # write_timeout: ENV['REDIS_MEMO_WRITE_TIMEOUT']&.to_f || 0.5,
68
+ # reconnect_attempts: ENV['REDIS_MEMO_RECONNECT_ATTEMPTS']&.to_i || 0
69
+ # }
70
+ # end
71
+ # end
72
+ # end
73
+ #
33
74
  def redis=(config)
34
75
  @redis_config = config
35
- @redis_client = nil
76
+ @redis = nil
36
77
  redis
37
78
  end
38
79
 
80
+ # Sets the tracer object. Allows the tracer to be dynamically determined at
81
+ # runtime if a blk is given.
39
82
  def tracer(&blk)
40
83
  if blk.nil?
41
84
  return @tracer if @tracer.respond_to?(:trace)
@@ -46,6 +89,8 @@ class RedisMemo::Options
46
89
  end
47
90
  end
48
91
 
92
+ # Sets the logger object in RedisMemo. Allows the logger to be dynamically
93
+ # determined at runtime if a blk is given.
49
94
  def logger(&blk)
50
95
  if blk.nil?
51
96
  return @logger if @logger.respond_to?(:warn)
@@ -56,6 +101,8 @@ class RedisMemo::Options
56
101
  end
57
102
  end
58
103
 
104
+ # Sets the global cache key version. Allows the logger to be dynamically
105
+ # determined at runtime if a blk is given.
59
106
  def global_cache_key_version(&blk)
60
107
  # this method takes a block to be consistent with the inline memo_method
61
108
  # API
@@ -71,16 +118,70 @@ class RedisMemo::Options
71
118
  end
72
119
  end
73
120
 
121
+ # Disables the model for caching and invalidation
122
+ def disable_model(model)
123
+ @disabled_models << model
124
+ end
125
+
126
+ # Checks if a model is disabled for redis memo caching
127
+ def model_disabled_for_caching?(model)
128
+ ENV["REDIS_MEMO_DISABLE_#{model.table_name.upcase}"] == 'true' || @disabled_models.include?(model)
129
+ end
130
+
131
+ # A handler used to asynchronously perform cache writes and invalidations. If no value is provided,
132
+ # RedisMemo will perform these operations synchronously.
74
133
  attr_accessor :async
134
+
135
+ # Specify the global sampled percentage of the chance to call the cache validation, a value between 0 to 100, when the value
136
+ # is 100, it will call the handler every time the cached result does not match the uncached result
137
+ # You can also specify inline cache validation sample percentage by memoize_method :method, cache_validation_sample_percentage: #{value}
138
+ attr_accessor :cache_validation_sample_percentage
139
+
140
+ # Handler called when the cached result does not match the uncached result (sampled at the
141
+ # `cache_validation_sample_percentage`). This might indicate that invalidation is happening too slowly or
142
+ # that there are incorrect dependencies specified on a cached method.
75
143
  attr_accessor :cache_out_of_date_handler
76
- attr_accessor :cache_validation_sampler
144
+
145
+ # Passed along to the Rails {RedisCacheStore}[https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html], determines whether or not to compress entries before storing
146
+ # them. default: `true`
77
147
  attr_accessor :compress
148
+
149
+ # Passed along to the Rails {RedisCacheStore}[https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html], the size threshold for which to compress cached entries.
150
+ # default: 1.kilobyte
78
151
  attr_accessor :compress_threshold
152
+
153
+ # Configuration values for connecting to RedisMemo using a connection pool. It's recommended to use a
154
+ # connection pool in multi-threaded applications, or when an async handler is set.
79
155
  attr_accessor :connection_pool
156
+
157
+ # Passed along to the Rails {RedisCacheStore}[https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html], sets the TTL on cache entries in Redis.
80
158
  attr_accessor :expires_in
159
+
160
+ # The max number of failed connection attempts RedisMemo will make for a single request before bypassing
161
+ # the caching layer. This helps make RedisMemo resilient to errors and performance issues when there's
162
+ # an issue with the Redis cluster itself.
163
+ attr_accessor :max_connection_attempts
164
+
165
+ # Passed along to the Rails {RedisCacheStore}[https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html], the error handler called for Redis related errors.
81
166
  attr_accessor :redis_error_handler
82
167
 
168
+ # A global kill switch to disable all RedisMemo operations.
169
+ attr_accessor :disable_all
170
+
171
+ # A kill switch to disable RedisMemo caching on database queries. This does not disable the invalidation
172
+ # after_save hooks that are installed on memoized models.
173
+ attr_accessor :disable_cached_select
174
+
175
+ # A kill switch to set the list of models to disable caching and invalidation after_save hooks on.
176
+ attr_accessor :disabled_models
177
+
178
+ # A global cache key version prepended to each cached entry. For example, the commit hash of the current
179
+ # version deployed to your application.
83
180
  attr_writer :global_cache_key_version
181
+
182
+ # Object used to trace RedisMemo operations to collect latency and error metrics, e.g. `Datadog.tracer`
84
183
  attr_writer :tracer
184
+
185
+ # Object used to log RedisMemo operations.
85
186
  attr_writer :logger
86
187
  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,7 +8,9 @@ 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|
@@ -30,6 +33,17 @@ class RedisMemo::Redis < Redis::Distributed
30
33
  super([], ring: hash_ring)
31
34
  end
32
35
 
36
+ def run_script(script_content, script_sha, *args)
37
+ begin
38
+ return evalsha(script_sha, *args) if script_sha
39
+ rescue Redis::CommandError => error
40
+ if error.message != 'NOSCRIPT No matching script. Please use EVAL.'
41
+ raise error
42
+ end
43
+ end
44
+ eval(script_content, *args) # rubocop: disable Security/Eval
45
+ end
46
+
33
47
  class WithReplicas < ::Redis
34
48
  def initialize(orig_options)
35
49
  options = orig_options.dup
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Redis memo can be flaky due to transient network errors (e.g. Redis connection errors), or when
4
+ # used with async handlers. This class allows users to override the default redis-memo behavior
5
+ # to be more robust when testing their code that uses redis-memo.
6
+ module RedisMemo
7
+ class Testing
8
+ class << self
9
+ attr_accessor :__test_mode
10
+ end
11
+
12
+ def self.enable_test_mode(&blk)
13
+ __set_test_mode(true, &blk)
14
+ end
15
+
16
+ def self.disable_test_mode(&blk)
17
+ __set_test_mode(false, &blk)
18
+ end
19
+
20
+ def self.enabled?
21
+ __test_mode
22
+ end
23
+
24
+ def self.__set_test_mode(mode, &blk)
25
+ if blk.nil?
26
+ __test_mode = mode
27
+ else
28
+ prev_mode = __test_mode
29
+ begin
30
+ __test_mode = mode
31
+ yield
32
+ ensure
33
+ __test_mode = prev_mode
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ module TestOverrides
40
+ def without_memoization?
41
+ if RedisMemo::Testing.enabled? && !RedisMemo::Memoizable::Invalidation.class_variable_get(:@@invalidation_queue).empty?
42
+ return true
43
+ end
44
+
45
+ super
46
+ end
47
+ end
48
+ singleton_class.prepend(TestOverrides)
49
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedisMemo::ThreadLocalVar
4
+ def self.define(var_name) # :nodoc:
5
+ thread_key = :"__redis_memo_#{var_name}__"
6
+ const_set(var_name.to_s.upcase, thread_key)
7
+
8
+ define_singleton_method var_name do
9
+ Thread.current[thread_key]
10
+ end
11
+
12
+ define_singleton_method "#{var_name}=" do |var_val|
13
+ Thread.current[thread_key] = var_val
14
+ end
15
+ end
16
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'options'
3
4
 
4
5
  class RedisMemo::Tracer
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedisMemo::Util
4
+ def self.checksum(serialized)
5
+ Digest::SHA1.base64digest(serialized)
6
+ end
7
+
8
+ def self.deep_sort_hash(orig_hash)
9
+ {}.tap do |new_hash|
10
+ orig_hash.sort.each do |k, v|
11
+ new_hash[k] = v.is_a?(Hash) ? deep_sort_hash(v) : v
12
+ end
13
+ end
14
+ end
15
+
16
+ def self.uuid
17
+ SecureRandom.uuid
18
+ end
19
+ 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.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chan Zuckerberg Initiative
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '5.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: connection_pool
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: 2.2.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 2.2.3
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: redis
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -39,19 +53,19 @@ dependencies:
39
53
  - !ruby/object:Gem::Version
40
54
  version: 4.0.1
41
55
  - !ruby/object:Gem::Dependency
42
- name: connection_pool
56
+ name: ruby2_keywords
43
57
  requirement: !ruby/object:Gem::Requirement
44
58
  requirements:
45
59
  - - ">="
46
60
  - !ruby/object:Gem::Version
47
- version: 2.2.3
61
+ version: '0'
48
62
  type: :runtime
49
63
  prerelease: false
50
64
  version_requirements: !ruby/object:Gem::Requirement
51
65
  requirements:
52
66
  - - ">="
53
67
  - !ruby/object:Gem::Version
54
- version: 2.2.3
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: activerecord
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +136,20 @@ dependencies:
122
136
  - - ">="
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: railties
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '5.2'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '5.2'
125
153
  - !ruby/object:Gem::Dependency
126
154
  name: rake
127
155
  requirement: !ruby/object:Gem::Requirement
@@ -150,6 +178,48 @@ dependencies:
150
178
  - - "~>"
151
179
  - !ruby/object:Gem::Version
152
180
  version: '3.2'
181
+ - !ruby/object:Gem::Dependency
182
+ name: rubocop
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ - !ruby/object:Gem::Dependency
196
+ name: rubocop-performance
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - ">="
207
+ - !ruby/object:Gem::Version
208
+ version: '0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: rubocop-rspec
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '0'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '0'
153
223
  - !ruby/object:Gem::Dependency
154
224
  name: simplecov
155
225
  requirement: !ruby/object:Gem::Requirement
@@ -176,8 +246,10 @@ files:
176
246
  - lib/redis_memo/batch.rb
177
247
  - lib/redis_memo/cache.rb
178
248
  - lib/redis_memo/connection_pool.rb
249
+ - lib/redis_memo/errors.rb
179
250
  - lib/redis_memo/future.rb
180
251
  - lib/redis_memo/memoizable.rb
252
+ - lib/redis_memo/memoizable/bump_version.lua
181
253
  - lib/redis_memo/memoizable/dependency.rb
182
254
  - lib/redis_memo/memoizable/invalidation.rb
183
255
  - lib/redis_memo/memoize_method.rb
@@ -191,8 +263,12 @@ files:
191
263
  - lib/redis_memo/memoize_query/model_callback.rb
192
264
  - lib/redis_memo/middleware.rb
193
265
  - lib/redis_memo/options.rb
266
+ - lib/redis_memo/railtie.rb
194
267
  - lib/redis_memo/redis.rb
268
+ - lib/redis_memo/testing.rb
269
+ - lib/redis_memo/thread_local_var.rb
195
270
  - lib/redis_memo/tracer.rb
271
+ - lib/redis_memo/util.rb
196
272
  homepage: https://github.com/chanzuckerberg/redis-memo
197
273
  licenses:
198
274
  - MIT