redis-memo 0.1.4 → 1.0.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.
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