redis-memo 0.1.4 → 1.0.0

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: a03c34771a13ad121b14359ba0e7b3596ef37da5cbf1f6c46e59bf28238e5e5d
4
- data.tar.gz: 55d84f1b82f40c261b0ff8a7a5faa868b50caf248e89e755b31a0011f1caab3f
3
+ metadata.gz: 8b3cb72a8a4bf3dcd6d30bb112e387710c65d09d60c3dc687e10db649da1e91a
4
+ data.tar.gz: b2452e1d4b2b7a3d588c13822eaffc1fb4336c3a4666f8a4ca473dbbd961578f
5
5
  SHA512:
6
- metadata.gz: 83638771de1cb0041c4864b71d53bb89c99508f2827db23b504f1751e1ad22cff7010451d5f47cdcb14ffb619162f16fe4cf177e403b0522e654b7e697b0b872
7
- data.tar.gz: fc5df8d6c8a336985927a2f35b177373a8f2e4dcd2b87bccac705440eaeaabe1a09f3d90138543c528b4763b70edc38931e6c9bc642e362b2c5ffef61e088d16
6
+ metadata.gz: 4d9193ddb53419db1e14a5eda4d4ab73b4a96620f592998c764abe3a118fb266761b04a929ff49d15194a9bcf959f9cb7c82f45f783387a85f4e1be2b1315ada
7
+ data.tar.gz: 383b2b053917ceeb3991de1c7c399fa83cd8e2bc326f735a0d0924767cea5d6157cf2f3e4a07b3d5d6524b54cae7e62276fb0723daad6a66ebaa6d0b6da64745
data/lib/redis_memo.rb CHANGED
@@ -3,11 +3,20 @@
3
3
  require 'active_support/all'
4
4
  require 'digest'
5
5
  require 'json'
6
+ require 'ruby2_keywords'
6
7
  require 'securerandom'
7
8
 
8
9
  module RedisMemo
10
+ require 'redis_memo/thread_local_var'
11
+
12
+ ThreadLocalVar.define :without_memoization
13
+ ThreadLocalVar.define :connection_attempts_count
14
+ ThreadLocalVar.define :max_connection_attempts
15
+
16
+ require 'redis_memo/errors'
9
17
  require 'redis_memo/memoize_method'
10
- require 'redis_memo/memoize_query'
18
+ require 'redis_memo/memoize_query' if defined?(ActiveRecord)
19
+ require 'redis_memo/railtie' if defined?(Rails) && defined?(Rails::Railtie)
11
20
 
12
21
  # A process-level +RedisMemo::Options+ instance that stores the global
13
22
  # options. This object can be modified by +RedisMemo.configure+.
@@ -17,11 +26,6 @@ module RedisMemo
17
26
  # +DefaultOptions+ as the default value.
18
27
  DefaultOptions = RedisMemo::Options.new
19
28
 
20
- # @todo Move thread keys to +RedisMemo::ThreadKey+
21
- THREAD_KEY_WITHOUT_MEMO = :__redis_memo_without_memo__
22
- THREAD_KEY_CONNECTION_ATTEMPTS_COUNT = :__redis_memo_connection_attempts_count__
23
- THREAD_KEY_MAX_CONNECTION_ATTEMPTS = :__redis_memo_max_connection_attempts__
24
-
25
29
  # Configure global-level default options. Those options will be used unless
26
30
  # some options specified at +memoize_method+ callsite level. See
27
31
  # +RedisMemo::Options+ for all the possible options.
@@ -37,10 +41,10 @@ module RedisMemo
37
41
  # to Redis.
38
42
  # - Batches cannot be nested
39
43
  # - When a batch is still open (while still in the +RedisMemo.batch+ block)
40
- # the return value of any memoized method is a +RedisMemo::Future+ instead of
41
- # the actual method value
44
+ # the return value of any memoized method is a +RedisMemo::Future+ instead of
45
+ # the actual method value
42
46
  # - The actual method values are returned as a list, in the same order as
43
- # invoking, after exiting the block
47
+ # invoking, after exiting the block
44
48
  #
45
49
  # @example
46
50
  # results = RedisMemo.batch do
@@ -58,75 +62,52 @@ module RedisMemo
58
62
  RedisMemo::Batch.close
59
63
  end
60
64
 
61
- # @todo Move this method out of the top namespace
62
- def self.checksum(serialized)
63
- Digest::SHA1.base64digest(serialized)
64
- end
65
-
66
- # @todo Move this method out of the top namespace
67
- def self.uuid
68
- SecureRandom.uuid
69
- end
70
-
71
- # @todo Move this method out of the top namespace
72
- def self.deep_sort_hash(orig_hash)
73
- {}.tap do |new_hash|
74
- orig_hash.sort.each do |k, v|
75
- new_hash[k] = v.is_a?(Hash) ? deep_sort_hash(v) : v
76
- end
77
- end
78
- end
79
-
80
65
  # Whether the current execution context has been configured to skip
81
66
  # memoization and use the uncached code path.
82
67
  #
83
68
  # @return [Boolean]
84
- def self.without_memo?
85
- ENV["REDIS_MEMO_DISABLE_ALL"] == 'true' || Thread.current[THREAD_KEY_WITHOUT_MEMO] == true
69
+ def self.without_memoization?
70
+ RedisMemo::DefaultOptions.disable_all || ThreadLocalVar.without_memoization == true
86
71
  end
87
72
 
88
73
  # Configure the wrapped code in the block to skip memoization.
89
74
  #
90
75
  # @yield [] no_args The block of code to skip memoization.
91
- def self.without_memo
92
- prev_value = Thread.current[THREAD_KEY_WITHOUT_MEMO]
93
- Thread.current[THREAD_KEY_WITHOUT_MEMO] = true
76
+ def self.without_memoization
77
+ prev_value = ThreadLocalVar.without_memoization
78
+ ThreadLocalVar.without_memoization = true
94
79
  yield
95
80
  ensure
96
- Thread.current[THREAD_KEY_WITHOUT_MEMO] = prev_value
81
+ ThreadLocalVar.without_memoization = prev_value
97
82
  end
98
83
 
99
- # Set the max connection attempts to Redis per code block. If we fail to connect to Redis more than `max_attempts`
100
- # times, the rest of the code block will fall back to the uncached flow, `RedisMemo.without_memo`.
84
+ # Set the max connection attempts to Redis per code block. If we fail to
85
+ # connect to Redis more than `max_attempts` times, the rest of the code block
86
+ # will fall back to the uncached flow, `RedisMemo.without_memoization`.
101
87
  #
102
88
  # @param [Integer] The max number of connection attempts.
103
89
  # @yield [] no_args the block of code to set the max attempts for.
104
90
  def self.with_max_connection_attempts(max_attempts)
105
- prev_value = Thread.current[THREAD_KEY_WITHOUT_MEMO]
106
- if max_attempts
107
- Thread.current[THREAD_KEY_CONNECTION_ATTEMPTS_COUNT] = 0
108
- Thread.current[THREAD_KEY_MAX_CONNECTION_ATTEMPTS] = max_attempts
109
- end
91
+ prev_value = ThreadLocalVar.without_memoization
92
+ ThreadLocalVar.connection_attempts_count = 0
93
+ ThreadLocalVar.max_connection_attempts = max_attempts
94
+
110
95
  yield
111
96
  ensure
112
- Thread.current[THREAD_KEY_WITHOUT_MEMO] = prev_value
113
- Thread.current[THREAD_KEY_CONNECTION_ATTEMPTS_COUNT] = nil
114
- Thread.current[THREAD_KEY_MAX_CONNECTION_ATTEMPTS] = nil
97
+ ThreadLocalVar.without_memoization = prev_value
98
+ ThreadLocalVar.connection_attempts_count = nil
99
+ ThreadLocalVar.max_connection_attempts = nil
115
100
  end
116
101
 
117
- private
118
- def self.incr_connection_attempts # :nodoc:
119
- return if Thread.current[THREAD_KEY_MAX_CONNECTION_ATTEMPTS].nil? || Thread.current[THREAD_KEY_CONNECTION_ATTEMPTS_COUNT].nil?
102
+ private_class_method def self.incr_connection_attempts # :nodoc:
103
+ return unless ThreadLocalVar.max_connection_attempts && ThreadLocalVar.connection_attempts_count
120
104
 
121
- # The connection attempts count and max connection attempts are reset in RedisMemo.with_max_connection_attempts
122
- Thread.current[THREAD_KEY_CONNECTION_ATTEMPTS_COUNT] += 1
123
- if Thread.current[THREAD_KEY_CONNECTION_ATTEMPTS_COUNT] >= Thread.current[THREAD_KEY_MAX_CONNECTION_ATTEMPTS]
124
- Thread.current[THREAD_KEY_WITHOUT_MEMO] = true
125
- end
126
- end
105
+ # The connection attempts count and max connection attempts are reset in
106
+ # RedisMemo.with_max_connection_attempts
107
+ ThreadLocalVar.connection_attempts_count += 1
108
+ return if ThreadLocalVar.connection_attempts_count <
109
+ ThreadLocalVar.max_connection_attempts
127
110
 
128
- # @todo Move errors to a separate file errors.rb
129
- class ArgumentError < ::ArgumentError; end
130
- class RuntimeError < ::RuntimeError; end
131
- class WithoutMemoization < Exception; end
111
+ ThreadLocalVar.without_memoization = true
112
+ end
132
113
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  # TODO: -> RedisMemo::Memoizable::AfterCommit
3
4
 
4
5
  class RedisMemo::AfterCommit
@@ -34,8 +35,6 @@ class RedisMemo::AfterCommit
34
35
  connection.transaction_open? && connection.current_transaction.joinable?
35
36
  end
36
37
 
37
- private
38
-
39
38
  def self.after_commit(&blk)
40
39
  connection.add_transaction_record(
41
40
  RedisMemo::AfterCommit::Callback.new(connection, committed: blk),
@@ -50,6 +49,7 @@ class RedisMemo::AfterCommit
50
49
 
51
50
  def self.reset_after_transaction
52
51
  return if @@callback_added
52
+
53
53
  @@callback_added = true
54
54
 
55
55
  after_commit do
@@ -1,30 +1,54 @@
1
- # frozen_string_literal: true
1
+ # frozen_string_literal: true
2
+
2
3
  require_relative 'cache'
3
4
  require_relative 'tracer'
4
5
 
6
+ ##
7
+ # This class facilitates the batching of Redis calls triggered by +memoize_method+
8
+ # to minimize the number of round trips to Redis.
9
+ #
10
+ # - Batches cannot be nested
11
+ # - When a batch is still open (while still in the +RedisMemo.batch+ block)
12
+ # the return value of any memoized method is a +RedisMemo::Future+ instead of
13
+ # the actual method value
14
+ # - The actual method values are returned as a list, in the same order as
15
+ # invoking, after exiting the block
16
+ #
17
+ # @example
18
+ # results = RedisMemo.batch do
19
+ # 5.times { |i| memoized_calculation(i) }
20
+ # nil # Not the return value of the block
21
+ # end
22
+ # results # [1,2,3,4,5] (results from the memoized_calculation calls)
5
23
  class RedisMemo::Batch
6
- THREAD_KEY = :__redis_memo_current_batch__
24
+ RedisMemo::ThreadLocalVar.define :batch
7
25
 
26
+ # Opens a new batch. If a batch is already open, raises an error
27
+ # to prevent nested batches.
8
28
  def self.open
9
29
  if current
10
- raise RedisMemo::RuntimeError, 'Batch can not be nested'
30
+ raise RedisMemo::RuntimeError.new('Batch can not be nested')
11
31
  end
12
32
 
13
- Thread.current[THREAD_KEY] = []
33
+ RedisMemo::ThreadLocalVar.batch = []
14
34
  end
15
35
 
36
+ # Closes the current batch, returning the futures in that batch.
16
37
  def self.close
17
- if current
18
- futures = current
19
- Thread.current[THREAD_KEY] = nil
20
- futures
21
- end
38
+ return unless current
39
+
40
+ futures = current
41
+ RedisMemo::ThreadLocalVar.batch = nil
42
+ futures
22
43
  end
23
44
 
45
+ # Retrieves the current open batch.
24
46
  def self.current
25
- Thread.current[THREAD_KEY]
47
+ RedisMemo::ThreadLocalVar.batch
26
48
  end
27
49
 
50
+ # Executes all the futures in the current batch using batched calls to
51
+ # Redis and closes it.
28
52
  def self.execute
29
53
  futures = close
30
54
  return unless futures
@@ -33,7 +57,8 @@ class RedisMemo::Batch
33
57
  method_cache_keys = nil
34
58
 
35
59
  RedisMemo::Tracer.trace('redis_memo.cache.batch.read', nil) do
36
- method_cache_keys = RedisMemo::MemoizeMethod.method_cache_keys(
60
+ method_cache_keys = RedisMemo::MemoizeMethod.__send__(
61
+ :method_cache_keys,
37
62
  futures.map(&:context),
38
63
  )
39
64
 
@@ -1,14 +1,17 @@
1
- # frozen_string_literal: true
1
+ # frozen_string_literal: true
2
+
2
3
  require_relative 'options'
3
4
  require_relative 'redis'
4
5
  require_relative 'connection_pool'
5
6
 
6
7
  class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
7
- class Rescuable < Exception; end
8
+ # This needs to be an Exception since RedisCacheStore rescues all
9
+ # RuntimeErrors
10
+ class Rescuable < Exception; end # rubocop:disable Lint/InheritException
8
11
 
9
- THREAD_KEY_LOCAL_CACHE = :__redis_memo_cache_local_cache__
10
- THREAD_KEY_LOCAL_DEPENDENCY_CACHE = :__redis_memo_local_cache_dependency_cache__
11
- THREAD_KEY_RAISE_ERROR = :__redis_memo_cache_raise_error__
12
+ RedisMemo::ThreadLocalVar.define :local_cache
13
+ RedisMemo::ThreadLocalVar.define :local_dependency_cache
14
+ RedisMemo::ThreadLocalVar.define :raise_error
12
15
 
13
16
  @@redis = nil
14
17
  @@redis_store = nil
@@ -17,13 +20,15 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
17
20
  RedisMemo::DefaultOptions.redis_error_handler&.call(exception, method)
18
21
  RedisMemo::DefaultOptions.logger&.warn(exception.full_message)
19
22
 
20
- RedisMemo.incr_connection_attempts if exception.is_a?(Redis::BaseConnectionError)
23
+ if exception.is_a?(Redis::BaseConnectionError)
24
+ RedisMemo.__send__(:incr_connection_attempts)
25
+ end
21
26
 
22
- if Thread.current[THREAD_KEY_RAISE_ERROR]
27
+ if RedisMemo::ThreadLocalVar.raise_error
23
28
  raise RedisMemo::Cache::Rescuable
24
- else
25
- returning
26
29
  end
30
+
31
+ returning
27
32
  end
28
33
 
29
34
  def self.redis
@@ -50,38 +55,38 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
50
55
  # ActiveSupport::Cache::Entry object -- which is slower comparing to a simple
51
56
  # hash storing object references
52
57
  def self.local_cache
53
- Thread.current[THREAD_KEY_LOCAL_CACHE]
58
+ RedisMemo::ThreadLocalVar.local_cache
54
59
  end
55
60
 
56
61
  def self.local_dependency_cache
57
- Thread.current[THREAD_KEY_LOCAL_DEPENDENCY_CACHE]
62
+ RedisMemo::ThreadLocalVar.local_dependency_cache
58
63
  end
59
64
 
60
65
  # See https://github.com/rails/rails/blob/fe76a95b0d252a2d7c25e69498b720c96b243ea2/activesupport/lib/active_support/cache/redis_cache_store.rb#L477
61
66
  # We overwrite this private method so we can also rescue ConnectionPool::TimeoutErrors
62
67
  def failsafe(method, returning: nil)
63
68
  yield
64
- rescue ::Redis::BaseError, ::ConnectionPool::TimeoutError => e
65
- handle_exception exception: e, method: method, returning: returning
69
+ rescue ::Redis::BaseError, ::ConnectionPool::TimeoutError => error
70
+ handle_exception exception: error, method: method, returning: returning
66
71
  returning
67
72
  end
68
73
  private :failsafe
69
74
 
70
75
  class << self
71
- def with_local_cache(&blk)
72
- Thread.current[THREAD_KEY_LOCAL_CACHE] = {}
73
- Thread.current[THREAD_KEY_LOCAL_DEPENDENCY_CACHE] = {}
74
- blk.call
76
+ def with_local_cache
77
+ RedisMemo::ThreadLocalVar.local_cache = {}
78
+ RedisMemo::ThreadLocalVar.local_dependency_cache = {}
79
+ yield
75
80
  ensure
76
- Thread.current[THREAD_KEY_LOCAL_CACHE] = nil
77
- Thread.current[THREAD_KEY_LOCAL_DEPENDENCY_CACHE] = nil
81
+ RedisMemo::ThreadLocalVar.local_cache = nil
82
+ RedisMemo::ThreadLocalVar.local_dependency_cache = nil
78
83
  end
79
84
 
80
85
  # RedisCacheStore doesn't read from the local cache before reading from redis
81
86
  def read_multi(*keys, raw: false, raise_error: false)
82
87
  return {} if keys.empty?
83
88
 
84
- Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
89
+ RedisMemo::ThreadLocalVar.raise_error = raise_error
85
90
 
86
91
  local_entries = local_cache&.slice(*keys) || {}
87
92
 
@@ -100,7 +105,7 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
100
105
  end
101
106
 
102
107
  def write(key, value, disable_async: false, raise_error: false, **options)
103
- Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
108
+ RedisMemo::ThreadLocalVar.raise_error = raise_error
104
109
 
105
110
  if local_cache
106
111
  local_cache[key] = value
@@ -111,7 +116,7 @@ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
111
116
  redis_store.write(key, value, **options)
112
117
  else
113
118
  async.call do
114
- Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
119
+ RedisMemo::ThreadLocalVar.raise_error = raise_error
115
120
  redis_store.write(key, value, **options)
116
121
  end
117
122
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require 'connection_pool'
3
4
  require_relative 'redis'
4
5
 
@@ -11,17 +12,17 @@ class RedisMemo::ConnectionPool
11
12
  end
12
13
 
13
14
  # Avoid method_missing when possible for better performance
14
- %i(get mget mapped_mget set eval).each do |method_name|
15
+ %i[get mget mapped_mget set eval evalsha run_script].each do |method_name|
15
16
  define_method method_name do |*args, &blk|
16
17
  @connection_pool.with do |redis|
17
- redis.send(method_name, *args, &blk)
18
+ redis.__send__(method_name, *args, &blk)
18
19
  end
19
20
  end
20
21
  end
21
22
 
22
23
  def method_missing(method_name, *args, &blk)
23
24
  @connection_pool.with do |redis|
24
- redis.send(method_name, *args, &blk)
25
+ redis.__send__(method_name, *args, &blk)
25
26
  end
26
27
  end
27
28
  end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RedisMemo
4
+ class ArgumentError < ::ArgumentError; end
5
+
6
+ class RuntimeError < ::RuntimeError; end
7
+
8
+ class WithoutMemoization < RuntimeError; end
9
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require_relative 'cache'
3
4
  require_relative 'tracer'
4
5
 
@@ -11,14 +12,14 @@ class RedisMemo::Future
11
12
  method_args,
12
13
  dependent_memos,
13
14
  cache_options,
14
- method_name_without_memo
15
+ method_name_without_memoization
15
16
  )
16
17
  @ref = ref
17
18
  @method_id = method_id
18
19
  @method_args = method_args
19
20
  @dependent_memos = dependent_memos
20
21
  @cache_options = cache_options
21
- @method_name_without_memo = method_name_without_memo
22
+ @method_name_without_memoization = method_name_without_memoization
22
23
  @method_cache_key = nil
23
24
  @cache_hit = false
24
25
  @cached_result = nil
@@ -33,18 +34,26 @@ class RedisMemo::Future
33
34
 
34
35
  def method_cache_key
35
36
  @method_cache_key ||=
36
- RedisMemo::MemoizeMethod.method_cache_keys([context])&.first || ''
37
+ RedisMemo::MemoizeMethod.__send__(:method_cache_keys, [context])&.first || ''
37
38
  end
38
39
 
39
- def execute(cached_results=nil)
40
+ def validate_cache_result
41
+ cache_validation_sample_percentage =
42
+ @cache_options[:cache_validation_sample_percentage] || RedisMemo::DefaultOptions.cache_validation_sample_percentage
43
+
44
+ if cache_validation_sample_percentage.nil?
45
+ false
46
+ else
47
+ cache_validation_sample_percentage > Random.rand(0...100)
48
+ end
49
+ end
50
+
51
+ def execute(cached_results = nil)
40
52
  if RedisMemo::Batch.current
41
- raise RedisMemo::RuntimeError, 'Cannot execute future when a batch is still open'
53
+ raise RedisMemo::RuntimeError.new('Cannot execute future when a batch is still open')
42
54
  end
43
55
 
44
56
  if cache_hit?(cached_results)
45
- validate_cache_result =
46
- RedisMemo::DefaultOptions.cache_validation_sampler&.call(@method_id)
47
-
48
57
  if validate_cache_result && cached_result != fresh_result
49
58
  RedisMemo::DefaultOptions.cache_out_of_date_handler&.call(
50
59
  @ref,
@@ -64,7 +73,7 @@ class RedisMemo::Future
64
73
 
65
74
  def result
66
75
  unless @computed_cached_result
67
- raise RedisMemo::RuntimeError, 'Future has not been executed'
76
+ raise RedisMemo::RuntimeError.new('Future has not been executed')
68
77
  end
69
78
 
70
79
  @fresh_result || @cached_result
@@ -72,13 +81,13 @@ class RedisMemo::Future
72
81
 
73
82
  private
74
83
 
75
- def cache_hit?(cached_results=nil)
84
+ def cache_hit?(cached_results = nil)
76
85
  cached_result(cached_results)
77
86
 
78
87
  @cache_hit
79
88
  end
80
89
 
81
- def cached_result(cached_results=nil)
90
+ def cached_result(cached_results = nil)
82
91
  return @cached_result if @computed_cached_result
83
92
 
84
93
  @cache_hit = false
@@ -102,7 +111,7 @@ class RedisMemo::Future
102
111
 
103
112
  RedisMemo::Tracer.trace('redis_memo.cache.write', @method_id) do
104
113
  # cache miss
105
- @fresh_result = @ref.send(@method_name_without_memo, *@method_args)
114
+ @fresh_result = @ref.__send__(@method_name_without_memoization, *@method_args)
106
115
  if @cache_options.include?(:expires_in) && @cache_options[:expires_in].respond_to?(:call)
107
116
  @cache_options[:expires_in] = @cache_options[:expires_in].call(@fresh_result)
108
117
  end
@@ -114,7 +123,7 @@ class RedisMemo::Future
114
123
  # result)
115
124
  @cache_hit && @cached_result != @fresh_result
116
125
  )
117
- )
126
+ )
118
127
  RedisMemo::Cache.write(method_cache_key, @fresh_result, **@cache_options)
119
128
  end
120
129
  end