redis-memo 0.0.0.alpha → 0.0.0.beta

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: 185b376e32235db7e679da783c652f4bb891afa70c771a32052211a3a1269164
4
- data.tar.gz: 00fe077bfe4615f8b96a0b0cb9510c48e5b4b9e5419ae1db0ce1ca4c2ccadbc1
3
+ metadata.gz: 1316982523363ce87b0ada30b82b8200559a07b2f1a6bea54168b40b2afd397e
4
+ data.tar.gz: 1278e1ff6b7cc2275416c7af9b8668a13128c105a4cde74640deccd0a4380373
5
5
  SHA512:
6
- metadata.gz: 87ec80320f71c9fe2eebd6f10acb86904feb0e400b66a9197a6374d54c47c31b45cf09a938dcff12aa5a50f631028e4f55a4390549f5f11a324aabcdef5fe28c
7
- data.tar.gz: ef51e78a7774686fb278ae122029ed24908dcaa5df936d05394ad364cb64fb99675f74e03843b6694d91d1c8c02f0ab8fc8ff638c79123701809e65224f5cd49
6
+ metadata.gz: 121069c8de985a8977234776ad15fd209c1c0db523c361a5caf9181ed7120533350af16c354d7f76dfc03168230248e37c64b37eb8ce9f6af16cfdc9160448f3
7
+ data.tar.gz: 133d18912a95eb599554b9cacb5c5932471772930165249381baaa8aeb403a3a6b21c6e17711a11eda56a1fa6dbf347396c7fe513cec3b53ec6f0c592d5b7d9a
@@ -0,0 +1 @@
1
+ require_relative 'redis_memo'
@@ -1,6 +1,52 @@
1
1
  # frozen_string_literal: true
2
-
3
- # typed: true
2
+ require 'active_support/all'
3
+ require 'digest'
4
+ require 'json'
4
5
 
5
6
  module RedisMemo
7
+ require 'redis_memo/memoize_method'
8
+ require 'redis_memo/memoize_records'
9
+
10
+ DefaultOptions = RedisMemo::Options.new
11
+
12
+ def self.configure(&blk)
13
+ blk.call(DefaultOptions)
14
+ end
15
+
16
+ def self.batch(&blk)
17
+ RedisMemo::Batch.open
18
+ blk.call
19
+ RedisMemo::Batch.execute
20
+ ensure
21
+ RedisMemo::Batch.close
22
+ end
23
+
24
+ def self.checksum(serialized)
25
+ Digest::SHA1.base64digest(serialized)
26
+ end
27
+
28
+ def self.deep_sort_hash(orig_hash)
29
+ {}.tap do |new_hash|
30
+ orig_hash.sort.each do |k, v|
31
+ new_hash[k] = v.is_a?(Hash) ? deep_sort_hash(v) : v
32
+ end
33
+ end
34
+ end
35
+
36
+ THREAD_KEY_WITHOUT_MEMO = :__redis_memo_without_memo__
37
+
38
+ def self.without_memo?
39
+ Thread.current[THREAD_KEY_WITHOUT_MEMO] == true
40
+ end
41
+
42
+ def self.without_memo
43
+ prev_value = Thread.current[THREAD_KEY_WITHOUT_MEMO]
44
+ Thread.current[THREAD_KEY_WITHOUT_MEMO] = true
45
+ yield
46
+ ensure
47
+ Thread.current[THREAD_KEY_WITHOUT_MEMO] = prev_value
48
+ end
49
+
50
+ class ArgumentError < ::ArgumentError; end
51
+ class RuntimeError < ::RuntimeError; end
6
52
  end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+ # TODO: -> RedisMemo::Memoizable::AfterCommit
3
+
4
+ class RedisMemo::AfterCommit
5
+ # We assume there's only one ActiveRecord DB connection used for opening
6
+ # transactions
7
+ @@callback_added = false
8
+ @@pending_memo_versions = {}
9
+ @@previous_memo_versions = {}
10
+
11
+ def self.connection
12
+ ActiveRecord::Base.connection
13
+ end
14
+
15
+ def self.pending_memo_versions
16
+ # In DB transactions, the pending memo version should be used immediately
17
+ # as part of method checksums. method_cache_keys made of
18
+ # pending_memo_versions are not referencable until we bump the
19
+ # pending_memo_versions after commiting the current transaction
20
+ @@pending_memo_versions
21
+ end
22
+
23
+ def self.bump_memo_version_after_commit(key, version, previous_version:)
24
+ @@pending_memo_versions[key] = version
25
+ @@previous_memo_versions[key] = previous_version
26
+
27
+ reset_after_transaction
28
+ end
29
+
30
+ # https://github.com/Envek/after_commit_everywhere/blob/be8602f9fbb8e40b0fc8a04a47e4c2bc6b560ad5/lib/after_commit_everywhere.rb#L93
31
+ # Helper method to determine whether we're currently in transaction or not
32
+ def self.in_transaction?
33
+ # service transactions (tests and database_cleaner) are not joinable
34
+ connection.transaction_open? && connection.current_transaction.joinable?
35
+ end
36
+
37
+ private
38
+
39
+ def self.after_commit(&blk)
40
+ connection.add_transaction_record(
41
+ RedisMemo::AfterCommit::Callback.new(connection, committed: blk),
42
+ )
43
+ end
44
+
45
+ def self.after_rollback(&blk)
46
+ connection.add_transaction_record(
47
+ RedisMemo::AfterCommit::Callback.new(connection, rolledback: blk),
48
+ )
49
+ end
50
+
51
+ def self.reset_after_transaction
52
+ return if @@callback_added
53
+ @@callback_added = true
54
+
55
+ after_commit do
56
+ reset(commited: true)
57
+ end
58
+
59
+ after_rollback do
60
+ reset(commited: false)
61
+ end
62
+ end
63
+
64
+ def self.reset(commited:)
65
+ if commited
66
+ @@pending_memo_versions.each do |key, version|
67
+ invalidation_queue =
68
+ RedisMemo::Memoizable::Invalidation.class_variable_get(:@@invalidation_queue)
69
+ invalidation_queue << [key, version, @@previous_memo_versions[key]]
70
+ end
71
+
72
+ RedisMemo::Memoizable::Invalidation.drain_invalidation_queue
73
+ else
74
+ @@pending_memo_versions.each_key do |key|
75
+ RedisMemo::Cache.local_cache&.delete(key)
76
+ end
77
+ end
78
+ @@callback_added = false
79
+ @@pending_memo_versions.clear
80
+ @@previous_memo_versions.clear
81
+ end
82
+
83
+ # https://github.com/Envek/after_commit_everywhere/blob/master/lib/after_commit_everywhere/wrap.rb
84
+ class Callback
85
+ def initialize(connection, committed: nil, rolledback: nil)
86
+ @connection = connection
87
+ @committed = committed
88
+ @rolledback = rolledback
89
+ end
90
+
91
+ def has_transactional_callbacks?
92
+ true
93
+ end
94
+
95
+ def trigger_transactional_callbacks?
96
+ true
97
+ end
98
+
99
+ def committed!(*)
100
+ @committed&.call
101
+ end
102
+
103
+ # Required for +transaction(requires_new: true)+
104
+ def add_to_transaction(*)
105
+ @connection.add_transaction_record(self)
106
+ end
107
+
108
+ def before_committed!(*); end
109
+
110
+ def rolledback!(*)
111
+ @rolledback&.call
112
+ end
113
+ end
114
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'cache'
3
+ require_relative 'tracer'
4
+
5
+ class RedisMemo::Batch
6
+ THREAD_KEY = :__redis_memo_current_batch__
7
+
8
+ def self.open
9
+ if current
10
+ raise RedisMemo::RuntimeError, 'Batch can not be nested'
11
+ end
12
+
13
+ Thread.current[THREAD_KEY] = []
14
+ end
15
+
16
+ def self.close
17
+ if current
18
+ futures = current
19
+ Thread.current[THREAD_KEY] = nil
20
+ futures
21
+ end
22
+ end
23
+
24
+ def self.current
25
+ Thread.current[THREAD_KEY]
26
+ end
27
+
28
+ def self.execute
29
+ futures = close
30
+ return unless futures
31
+
32
+ cached_results = {}
33
+ method_cache_keys = nil
34
+
35
+ RedisMemo::Tracer.trace('redis_memo.cache.batch.read', nil) do
36
+ method_cache_keys = RedisMemo::MemoizeMethod.method_cache_keys(
37
+ futures.map(&:context),
38
+ )
39
+
40
+ if method_cache_keys
41
+ cached_results = RedisMemo::Cache.read_multi(*method_cache_keys)
42
+ end
43
+ end
44
+
45
+ RedisMemo::Tracer.trace('redis_memo.cache.batch.execute', nil) do
46
+ results = Array.new(futures.size)
47
+ futures.each_with_index do |future, i|
48
+ future.method_cache_key = method_cache_keys ? method_cache_keys[i] : ''
49
+ results[i] = future.execute(cached_results)
50
+ end
51
+ results
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'options'
3
+ require_relative 'redis'
4
+
5
+ class RedisMemo::Cache < ActiveSupport::Cache::RedisCacheStore
6
+ class Rescuable < Exception; end
7
+
8
+ THREAD_KEY_LOCAL_CACHE = :__redis_memo_cache_local_cache__
9
+ THREAD_KEY_RAISE_ERROR = :__redis_memo_cache_raise_error__
10
+
11
+ @@redis = nil
12
+ @@redis_store = nil
13
+
14
+ @@redis_store_error_handler = proc do |method:, exception:, returning:|
15
+ RedisMemo::DefaultOptions.redis_error_handler&.call(exception, method)
16
+ RedisMemo::DefaultOptions.logger&.warn(exception.full_message)
17
+
18
+ if Thread.current[THREAD_KEY_RAISE_ERROR]
19
+ raise RedisMemo::Cache::Rescuable
20
+ else
21
+ returning
22
+ end
23
+ end
24
+
25
+ def self.redis
26
+ @@redis ||= RedisMemo::DefaultOptions.redis
27
+ end
28
+
29
+ def self.redis_store
30
+ @@redis_store ||= new(
31
+ compress: RedisMemo::DefaultOptions.compress,
32
+ compress_threshold: RedisMemo::DefaultOptions.compress_threshold,
33
+ error_handler: @@redis_store_error_handler,
34
+ expires_in: RedisMemo::DefaultOptions.expires_in,
35
+ redis: redis,
36
+ )
37
+ end
38
+
39
+ # We use our own local_cache instead of the one from RedisCacheStore, because
40
+ # the local_cache in RedisCacheStore stores a dumped
41
+ # ActiveSupport::Cache::Entry object -- which is slower comparing to a simple
42
+ # hash storing object references
43
+ def self.local_cache
44
+ Thread.current[THREAD_KEY_LOCAL_CACHE]
45
+ end
46
+
47
+ class << self
48
+ def with_local_cache(&blk)
49
+ Thread.current[THREAD_KEY_LOCAL_CACHE] = {}
50
+ blk.call
51
+ ensure
52
+ Thread.current[THREAD_KEY_LOCAL_CACHE] = nil
53
+ end
54
+
55
+ # RedisCacheStore doesn't read from the local cache before reading from redis
56
+ def read_multi(*keys, raise_error: false)
57
+ return {} if keys.empty?
58
+
59
+ Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
60
+
61
+ local_entries = local_cache&.slice(*keys) || {}
62
+
63
+ keys_to_fetch = keys
64
+ keys_to_fetch -= local_entries.keys unless local_entries.empty?
65
+ return local_entries if keys_to_fetch.empty?
66
+
67
+ remote_entries = redis_store.read_multi(*keys_to_fetch)
68
+ local_cache&.merge!(remote_entries)
69
+
70
+ if local_entries.empty?
71
+ remote_entries
72
+ else
73
+ local_entries.merge!(remote_entries)
74
+ end
75
+ end
76
+
77
+ def write(key, value, disable_async: false, raise_error: false, **options)
78
+ Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
79
+
80
+ if local_cache
81
+ local_cache[key] = value
82
+ end
83
+
84
+ async = RedisMemo::DefaultOptions.async
85
+ if async.nil? || disable_async
86
+ redis_store.write(key, value, **options)
87
+ else
88
+ async.call do
89
+ Thread.current[THREAD_KEY_RAISE_ERROR] = raise_error
90
+ redis_store.write(key, value, **options)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,125 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'cache'
3
+ require_relative 'tracer'
4
+
5
+ class RedisMemo::Future
6
+ attr_writer :method_cache_key
7
+
8
+ def initialize(
9
+ ref,
10
+ method_id,
11
+ method_args,
12
+ depends_on,
13
+ cache_options,
14
+ method_name_without_memo
15
+ )
16
+ @ref = ref
17
+ @method_id = method_id
18
+ @method_args = method_args
19
+ @depends_on = depends_on
20
+ @cache_options = cache_options
21
+ @method_name_without_memo = method_name_without_memo
22
+ @method_cache_key = nil
23
+ @cache_hit = false
24
+ @cached_result = nil
25
+ @computed_cached_result = false
26
+ @fresh_result = nil
27
+ @computed_fresh_result = false
28
+ end
29
+
30
+ def context
31
+ [@ref, @method_id, @method_args, @depends_on]
32
+ end
33
+
34
+ def method_cache_key
35
+ @method_cache_key ||=
36
+ RedisMemo::MemoizeMethod.method_cache_keys([context])&.first || ''
37
+ end
38
+
39
+ def execute(cached_results=nil)
40
+ if RedisMemo::Batch.current
41
+ raise RedisMemo::RuntimeError, 'Cannot execute future when a batch is still open'
42
+ end
43
+
44
+ if cache_hit?(cached_results)
45
+ validate_cache_result =
46
+ RedisMemo::DefaultOptions.cache_validation_sampler&.call(@method_id)
47
+
48
+ if validate_cache_result && cached_result != fresh_result
49
+ RedisMemo::DefaultOptions.cache_out_of_date_handler&.call(
50
+ @ref,
51
+ @method_id,
52
+ @method_args,
53
+ cached_result,
54
+ fresh_result,
55
+ )
56
+ return fresh_result
57
+ end
58
+
59
+ return cached_result
60
+ end
61
+
62
+ fresh_result
63
+ end
64
+
65
+ def result
66
+ unless @computed_cached_result
67
+ raise RedisMemo::RuntimeError, 'Future has not been executed'
68
+ end
69
+
70
+ @fresh_result || @cached_result
71
+ end
72
+
73
+ private
74
+
75
+ def cache_hit?(cached_results=nil)
76
+ cached_result(cached_results)
77
+
78
+ @cache_hit
79
+ end
80
+
81
+ def cached_result(cached_results=nil)
82
+ return @cached_result if @computed_cached_result
83
+
84
+ @cache_hit = false
85
+ RedisMemo::Tracer.trace('redis_memo.cache.read', @method_id) do
86
+ # Calculate the method_cache_key now, or use the method_cache_key obtained
87
+ # from batching previously
88
+ if !method_cache_key.empty?
89
+ cached_results ||= RedisMemo::Cache.read_multi(method_cache_key)
90
+ @cache_hit = cached_results.include?(method_cache_key)
91
+ @cached_result = cached_results[method_cache_key]
92
+ end
93
+ RedisMemo::Tracer.set_tag(cache_hit: @cache_hit)
94
+ end
95
+
96
+ @computed_cached_result = true
97
+ @cached_result
98
+ end
99
+
100
+ def fresh_result
101
+ return @fresh_result if @computed_fresh_result
102
+
103
+ RedisMemo::Tracer.trace('redis_memo.cache.write', @method_id) do
104
+ # cache miss
105
+ @fresh_result = @ref.send(@method_name_without_memo, *@method_args)
106
+ if @cache_options.include?(:expires_in) && @cache_options[:expires_in].respond_to?(:call)
107
+ @cache_options[:expires_in] = @cache_options[:expires_in].call(@fresh_result)
108
+ end
109
+
110
+ if !method_cache_key.empty? && (
111
+ # Write back fresh result if cache miss
112
+ !@cache_hit || (
113
+ # or cached result is out of date (sampled to validate the cache
114
+ # result)
115
+ @cache_hit && @cached_result != @fresh_result
116
+ )
117
+ )
118
+ RedisMemo::Cache.write(method_cache_key, @fresh_result, **@cache_options)
119
+ end
120
+ end
121
+
122
+ @computed_fresh_result = true
123
+ @fresh_result
124
+ end
125
+ end