atomic_cache 0.2.4.rc1 → 0.2.5.rc1
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 +4 -4
- data/lib/atomic_cache/atomic_cache_client.rb +7 -8
- data/lib/atomic_cache/version.rb +1 -1
- data/spec/atomic_cache/integration/waiting_spec.rb +102 -0
- metadata +10 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5c6e0bf96718fb99b6047009b67c12f3fb4da0b36fd18ba8eb7aded71b744cb2
|
|
4
|
+
data.tar.gz: 8443307bb59ff3e3ca393cb6401f6c30974ec97bc3729024848be74504c5abda
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f438b868a3b60d2d64be72fa314f8cb4ca2a04d9972286d623393bf9754b488577f150d4726c26ee236a0a0cbaae9444fc892fc808b8cda3b23052e2adb8fb8d
|
|
7
|
+
data.tar.gz: d84517bed76804507f0dc395205eaad4e114e0dfda3023b33f6a1e1191e396d1643b7fb501ebd66e677d8d682ef822b5fa5329660ee5c0c2947d3adb4009c99c
|
|
@@ -27,7 +27,6 @@ module AtomicCache
|
|
|
27
27
|
raise ArgumentError.new("`storage` required but none given") unless @storage.present?
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
31
30
|
# Attempts to fetch the given keyspace, using an optional block to generate
|
|
32
31
|
# a new value when the cache is expired
|
|
33
32
|
#
|
|
@@ -60,13 +59,13 @@ module AtomicCache
|
|
|
60
59
|
|
|
61
60
|
# quick check back to see if the other process has finished
|
|
62
61
|
# or fall back to the last known value
|
|
63
|
-
value = quick_retry(
|
|
62
|
+
value = quick_retry(key, options, tags) || last_known_value(keyspace, options, tags)
|
|
64
63
|
return value if value.present?
|
|
65
64
|
|
|
66
65
|
# wait for the other process if a last known value isn't there
|
|
67
66
|
if key.present?
|
|
68
67
|
return time('wait.run', tags: tags) do
|
|
69
|
-
wait_for_new_value(
|
|
68
|
+
wait_for_new_value(keyspace, options, tags)
|
|
70
69
|
end
|
|
71
70
|
end
|
|
72
71
|
|
|
@@ -110,10 +109,8 @@ module AtomicCache
|
|
|
110
109
|
nil
|
|
111
110
|
end
|
|
112
111
|
|
|
113
|
-
def quick_retry(
|
|
114
|
-
key = @timestamp_manager.current_key(keyspace)
|
|
112
|
+
def quick_retry(key, options, tags)
|
|
115
113
|
duration = option(:quick_retry_ms, options, DEFAULT_quick_retry_ms)
|
|
116
|
-
|
|
117
114
|
if duration.present? and key.present?
|
|
118
115
|
sleep(duration.to_f / 1000)
|
|
119
116
|
value = @storage.read(key, options)
|
|
@@ -151,7 +148,7 @@ module AtomicCache
|
|
|
151
148
|
nil
|
|
152
149
|
end
|
|
153
150
|
|
|
154
|
-
def wait_for_new_value(
|
|
151
|
+
def wait_for_new_value(keyspace, options, tags)
|
|
155
152
|
max_retries = option(:max_retries, options, DEFAULT_MAX_RETRIES)
|
|
156
153
|
max_retries.times do |attempt|
|
|
157
154
|
metrics_tags = tags.clone.push("attempt:#{attempt}")
|
|
@@ -162,6 +159,8 @@ module AtomicCache
|
|
|
162
159
|
backoff_duration_ms = option(:backoff_duration_ms, options, backoff_duration_ms)
|
|
163
160
|
sleep((backoff_duration_ms.to_f / 1000) * attempt)
|
|
164
161
|
|
|
162
|
+
# re-fetch the key each time, to make sure we're actually getting the latest key with the correct LMT
|
|
163
|
+
key = @timestamp_manager.current_key(keyspace)
|
|
165
164
|
value = @storage.read(key, options)
|
|
166
165
|
if !value.nil?
|
|
167
166
|
metrics(:increment, 'wait.present', tags: metrics_tags)
|
|
@@ -170,7 +169,7 @@ module AtomicCache
|
|
|
170
169
|
end
|
|
171
170
|
|
|
172
171
|
metrics(:increment, 'wait.give-up')
|
|
173
|
-
log(:warn, "Giving up
|
|
172
|
+
log(:warn, "Giving up waiting. Exceeded max retries (#{max_retries}).")
|
|
174
173
|
nil
|
|
175
174
|
end
|
|
176
175
|
|
data/lib/atomic_cache/version.rb
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe 'Integration' do
|
|
6
|
+
let(:key_storage) { AtomicCache::Storage::SharedMemory.new }
|
|
7
|
+
let(:cache_storage) { AtomicCache::Storage::SharedMemory.new }
|
|
8
|
+
let(:keyspace) { AtomicCache::Keyspace.new(namespace: 'int.waiting') }
|
|
9
|
+
let(:timestamp_manager) { AtomicCache::LastModTimeKeyManager.new(keyspace: keyspace, storage: key_storage) }
|
|
10
|
+
|
|
11
|
+
let(:generating_client) { AtomicCache::AtomicCacheClient.new(storage: cache_storage, timestamp_manager: timestamp_manager) }
|
|
12
|
+
let(:waiting_client) { AtomicCache::AtomicCacheClient.new(storage: cache_storage, timestamp_manager: timestamp_manager) }
|
|
13
|
+
|
|
14
|
+
it 'correctly waits for a key when no last know value is available' do
|
|
15
|
+
generating_thread = ClientThread.new(generating_client, keyspace)
|
|
16
|
+
generating_thread.start
|
|
17
|
+
waiting_thread = ClientThread.new(waiting_client, keyspace)
|
|
18
|
+
waiting_thread.start
|
|
19
|
+
|
|
20
|
+
generating_thread.generate
|
|
21
|
+
sleep 0.05
|
|
22
|
+
waiting_thread.fetch
|
|
23
|
+
sleep 0.05
|
|
24
|
+
generating_thread.complete
|
|
25
|
+
sleep 0.05
|
|
26
|
+
|
|
27
|
+
generating_thread.terminate
|
|
28
|
+
waiting_thread.terminate
|
|
29
|
+
|
|
30
|
+
expect(generating_thread.result).to eq([1, 2, 3])
|
|
31
|
+
expect(waiting_thread.result).to eq([1, 2, 3])
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Avert your eyes:
|
|
37
|
+
# this class allows atomic client interaction to happen asynchronously so that
|
|
38
|
+
# the waiting behavior of the client can be tested simultaneous to controlling how
|
|
39
|
+
# long the 'generate' behavior takes
|
|
40
|
+
#
|
|
41
|
+
# It works by accepting an incoming 'message' which it places onto one of two queues
|
|
42
|
+
class ClientThread
|
|
43
|
+
attr_reader :result
|
|
44
|
+
|
|
45
|
+
def initialize(client, keyspace)
|
|
46
|
+
@keyspace = keyspace
|
|
47
|
+
@client = client
|
|
48
|
+
@msg_queue = Queue.new
|
|
49
|
+
@generate_queue = Queue.new
|
|
50
|
+
@result = nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def start
|
|
54
|
+
@thread = Thread.new(&method(:run))
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fetch
|
|
58
|
+
@msg_queue << :fetch
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def generate
|
|
62
|
+
@msg_queue << :generate
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def complete
|
|
66
|
+
@generate_queue << :complete
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def terminate
|
|
70
|
+
@msg_queue << :terminate
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def run
|
|
76
|
+
loop do
|
|
77
|
+
msg = @msg_queue.pop
|
|
78
|
+
sleep 0.001; next unless msg
|
|
79
|
+
|
|
80
|
+
case msg
|
|
81
|
+
when :terminate
|
|
82
|
+
Thread.stop
|
|
83
|
+
when :generate
|
|
84
|
+
do_generate
|
|
85
|
+
when :fetch
|
|
86
|
+
@result = @client.fetch(@keyspace)
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def do_generate
|
|
92
|
+
@client.fetch(@keyspace) do
|
|
93
|
+
loop do
|
|
94
|
+
msg = @generate_queue.pop
|
|
95
|
+
sleep 0.001; next unless msg
|
|
96
|
+
break if msg == :complete
|
|
97
|
+
end
|
|
98
|
+
@result = [1, 2, 3] # generated value
|
|
99
|
+
@result
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: atomic_cache
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.5.rc1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ibotta Developers
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date: 2021-06-
|
|
12
|
+
date: 2021-06-28 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: bundler
|
|
@@ -171,7 +171,8 @@ dependencies:
|
|
|
171
171
|
- - "~>"
|
|
172
172
|
- !ruby/object:Gem::Version
|
|
173
173
|
version: '0.1'
|
|
174
|
-
description:
|
|
174
|
+
description: A gem which prevents the thundering herd problem through a distributed
|
|
175
|
+
lock
|
|
175
176
|
email: osscompliance@ibotta.com
|
|
176
177
|
executables: []
|
|
177
178
|
extensions: []
|
|
@@ -200,6 +201,7 @@ files:
|
|
|
200
201
|
- spec/atomic_cache/atomic_cache_client_spec.rb
|
|
201
202
|
- spec/atomic_cache/concerns/global_lmt_cache_concern_spec.rb
|
|
202
203
|
- spec/atomic_cache/default_config_spec.rb
|
|
204
|
+
- spec/atomic_cache/integration/waiting_spec.rb
|
|
203
205
|
- spec/atomic_cache/key/keyspace_spec.rb
|
|
204
206
|
- spec/atomic_cache/key/last_mod_time_key_manager_spec.rb
|
|
205
207
|
- spec/atomic_cache/storage/dalli_spec.rb
|
|
@@ -229,5 +231,9 @@ requirements: []
|
|
|
229
231
|
rubygems_version: 3.0.8
|
|
230
232
|
signing_key:
|
|
231
233
|
specification_version: 4
|
|
232
|
-
summary:
|
|
234
|
+
summary: In a nutshell:* The key of every cached value includes a timestamp* Once
|
|
235
|
+
a cache key is written to, it is never written over* When a newer version of a cached
|
|
236
|
+
value is available, it is written to a new key* When a new value is being generated
|
|
237
|
+
for a new key only 1 process is allowed to do so at a time* While the new value
|
|
238
|
+
is being generated, other processes read one key older than most recent
|
|
233
239
|
test_files: []
|