redis-memo 0.0.0.alpha → 0.0.0.beta

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: 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