atomic_cache 0.2.1.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/docs/USAGE.md +8 -0
- data/lib/atomic_cache/atomic_cache_client.rb +9 -8
- data/lib/atomic_cache/storage/dalli.rb +5 -8
- data/lib/atomic_cache/version.rb +1 -1
- data/spec/atomic_cache/default_config_spec.rb +6 -1
- data/spec/atomic_cache/integration/waiting_spec.rb +102 -0
- data/spec/atomic_cache/storage/dalli_spec.rb +14 -12
- 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
|
data/docs/USAGE.md
CHANGED
|
@@ -21,6 +21,14 @@ end
|
|
|
21
21
|
|
|
22
22
|
In addition to the below options, any other options given (e.g. `expires_in`, `cache_nils`) are passed through to the underlying storage adapter. This allows storage-specific options to be passed through (reference: [Dalli config](https://github.com/petergoldstein/dalli#configuration)).
|
|
23
23
|
|
|
24
|
+
#### TTL
|
|
25
|
+
Various storage clients require TTL to be expressed in different ways. The included storage adapters will unwrap the `ttl` option to an storage-specific representation.
|
|
26
|
+
```ruby
|
|
27
|
+
atomic_cache.fetch(ttl: 500) do
|
|
28
|
+
# generate block
|
|
29
|
+
end
|
|
30
|
+
```
|
|
31
|
+
|
|
24
32
|
#### `generate_ttl_ms`
|
|
25
33
|
_Defaults to 30 seconds._
|
|
26
34
|
|
|
@@ -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
|
#
|
|
@@ -45,6 +44,7 @@ module AtomicCache
|
|
|
45
44
|
value = @storage.read(key, options) if key.present?
|
|
46
45
|
if !value.nil?
|
|
47
46
|
metrics(:increment, 'read.present', tags: tags)
|
|
47
|
+
log(:debug, "Read value from key: '#{key}'")
|
|
48
48
|
return value
|
|
49
49
|
end
|
|
50
50
|
|
|
@@ -59,13 +59,13 @@ module AtomicCache
|
|
|
59
59
|
|
|
60
60
|
# quick check back to see if the other process has finished
|
|
61
61
|
# or fall back to the last known value
|
|
62
|
-
value = quick_retry(
|
|
62
|
+
value = quick_retry(key, options, tags) || last_known_value(keyspace, options, tags)
|
|
63
63
|
return value if value.present?
|
|
64
64
|
|
|
65
65
|
# wait for the other process if a last known value isn't there
|
|
66
66
|
if key.present?
|
|
67
67
|
return time('wait.run', tags: tags) do
|
|
68
|
-
wait_for_new_value(
|
|
68
|
+
wait_for_new_value(keyspace, options, tags)
|
|
69
69
|
end
|
|
70
70
|
end
|
|
71
71
|
|
|
@@ -109,10 +109,8 @@ module AtomicCache
|
|
|
109
109
|
nil
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
def quick_retry(
|
|
113
|
-
key = @timestamp_manager.current_key(keyspace)
|
|
112
|
+
def quick_retry(key, options, tags)
|
|
114
113
|
duration = option(:quick_retry_ms, options, DEFAULT_quick_retry_ms)
|
|
115
|
-
|
|
116
114
|
if duration.present? and key.present?
|
|
117
115
|
sleep(duration.to_f / 1000)
|
|
118
116
|
value = @storage.read(key, options)
|
|
@@ -136,6 +134,7 @@ module AtomicCache
|
|
|
136
134
|
# last known key may have expired
|
|
137
135
|
if !lkv.nil?
|
|
138
136
|
metrics(:increment, 'last-known-value.present', tags: tags)
|
|
137
|
+
log(:debug, "Read value from last known value key: '#{lkk}'")
|
|
139
138
|
return lkv
|
|
140
139
|
end
|
|
141
140
|
|
|
@@ -149,7 +148,7 @@ module AtomicCache
|
|
|
149
148
|
nil
|
|
150
149
|
end
|
|
151
150
|
|
|
152
|
-
def wait_for_new_value(
|
|
151
|
+
def wait_for_new_value(keyspace, options, tags)
|
|
153
152
|
max_retries = option(:max_retries, options, DEFAULT_MAX_RETRIES)
|
|
154
153
|
max_retries.times do |attempt|
|
|
155
154
|
metrics_tags = tags.clone.push("attempt:#{attempt}")
|
|
@@ -160,6 +159,8 @@ module AtomicCache
|
|
|
160
159
|
backoff_duration_ms = option(:backoff_duration_ms, options, backoff_duration_ms)
|
|
161
160
|
sleep((backoff_duration_ms.to_f / 1000) * attempt)
|
|
162
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)
|
|
163
164
|
value = @storage.read(key, options)
|
|
164
165
|
if !value.nil?
|
|
165
166
|
metrics(:increment, 'wait.present', tags: metrics_tags)
|
|
@@ -168,7 +169,7 @@ module AtomicCache
|
|
|
168
169
|
end
|
|
169
170
|
|
|
170
171
|
metrics(:increment, 'wait.give-up')
|
|
171
|
-
log(:warn, "Giving up
|
|
172
|
+
log(:warn, "Giving up waiting. Exceeded max retries (#{max_retries}).")
|
|
172
173
|
nil
|
|
173
174
|
end
|
|
174
175
|
|
|
@@ -10,10 +10,6 @@ module AtomicCache
|
|
|
10
10
|
class Dalli < Store
|
|
11
11
|
extend Forwardable
|
|
12
12
|
|
|
13
|
-
ADD_SUCCESS = 'STORED'
|
|
14
|
-
ADD_UNSUCCESSFUL = 'NOT_STORED'
|
|
15
|
-
ADD_EXISTS = 'EXISTS'
|
|
16
|
-
|
|
17
13
|
def_delegators :@dalli_client, :delete
|
|
18
14
|
|
|
19
15
|
def initialize(dalli_client)
|
|
@@ -27,16 +23,17 @@ module AtomicCache
|
|
|
27
23
|
# dalli expects time in seconds
|
|
28
24
|
# https://github.com/petergoldstein/dalli/blob/b8f4afe165fb3e07294c36fb1c63901b0ed9ce10/lib/dalli/client.rb#L27
|
|
29
25
|
# TODO: verify this unit is being treated correctly through the system
|
|
30
|
-
|
|
31
|
-
response.start_with?(ADD_SUCCESS)
|
|
26
|
+
!!@dalli_client.add(key, new_value, ttl, opts)
|
|
32
27
|
end
|
|
33
28
|
|
|
34
29
|
def read(key, user_options={})
|
|
35
|
-
@dalli_client.
|
|
30
|
+
@dalli_client.get(key, user_options)
|
|
36
31
|
end
|
|
37
32
|
|
|
38
33
|
def set(key, value, user_options={})
|
|
39
|
-
|
|
34
|
+
ttl = user_options[:ttl]
|
|
35
|
+
user_options.delete(:ttl)
|
|
36
|
+
!!@dalli_client.set(key, value, ttl, user_options)
|
|
40
37
|
end
|
|
41
38
|
|
|
42
39
|
end
|
data/lib/atomic_cache/version.rb
CHANGED
|
@@ -10,8 +10,13 @@ describe 'DefaultConfig' do
|
|
|
10
10
|
subject.configure do |manager|
|
|
11
11
|
manager.namespace = 'foo'
|
|
12
12
|
end
|
|
13
|
-
|
|
14
13
|
expect(subject.instance.namespace).to eq('foo')
|
|
14
|
+
|
|
15
|
+
# change it a 2nd time to make sure it sticks
|
|
16
|
+
subject.configure do |manager|
|
|
17
|
+
manager.namespace = 'bar'
|
|
18
|
+
end
|
|
19
|
+
expect(subject.instance.namespace).to eq('bar')
|
|
15
20
|
end
|
|
16
21
|
end
|
|
17
22
|
end
|
|
@@ -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
|
|
@@ -13,13 +13,20 @@ describe 'Dalli' do
|
|
|
13
13
|
let(:dalli_client) { FakeDalli.new }
|
|
14
14
|
subject { AtomicCache::Storage::Dalli.new(dalli_client) }
|
|
15
15
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
context '#set' do
|
|
17
|
+
it 'delegates #set without options' do
|
|
18
|
+
expect(dalli_client).to receive(:set).with('key', 'value', nil, {})
|
|
19
|
+
subject.set('key', 'value')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
it 'delegates #set with TTL' do
|
|
23
|
+
expect(dalli_client).to receive(:set).with('key', 'value', 500, {})
|
|
24
|
+
subject.set('key', 'value', { ttl: 500 })
|
|
25
|
+
end
|
|
19
26
|
end
|
|
20
27
|
|
|
21
28
|
it 'delegates #read without options' do
|
|
22
|
-
expect(dalli_client).to receive(:
|
|
29
|
+
expect(dalli_client).to receive(:get).with('key', {}).and_return('asdf')
|
|
23
30
|
subject.read('key')
|
|
24
31
|
end
|
|
25
32
|
|
|
@@ -30,7 +37,7 @@ describe 'Dalli' do
|
|
|
30
37
|
|
|
31
38
|
context '#add' do
|
|
32
39
|
before(:each) do
|
|
33
|
-
allow(dalli_client).to receive(:add).and_return(
|
|
40
|
+
allow(dalli_client).to receive(:add).and_return(false)
|
|
34
41
|
end
|
|
35
42
|
|
|
36
43
|
it 'delegates to #add with the raw option set' do
|
|
@@ -40,22 +47,17 @@ describe 'Dalli' do
|
|
|
40
47
|
end
|
|
41
48
|
|
|
42
49
|
it 'returns true when the add is successful' do
|
|
43
|
-
expect(dalli_client).to receive(:add).and_return(
|
|
50
|
+
expect(dalli_client).to receive(:add).and_return(12339031748204560384)
|
|
44
51
|
result = subject.add('key', 'value', 100)
|
|
45
52
|
expect(result).to eq(true)
|
|
46
53
|
end
|
|
47
54
|
|
|
48
55
|
it 'returns false if the key already exists' do
|
|
49
|
-
expect(dalli_client).to receive(:add).and_return(
|
|
56
|
+
expect(dalli_client).to receive(:add).and_return(false)
|
|
50
57
|
result = subject.add('key', 'value', 100)
|
|
51
58
|
expect(result).to eq(false)
|
|
52
59
|
end
|
|
53
60
|
|
|
54
|
-
it 'returns false if the add fails' do
|
|
55
|
-
expect(dalli_client).to receive(:add).and_return('NOT_STORED\r\n')
|
|
56
|
-
result = subject.add('key', 'value', 100)
|
|
57
|
-
expect(result).to eq(false)
|
|
58
|
-
end
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
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: []
|