atomic_cache 0.4.0.rc1 → 0.5.2.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/docs/USAGE.md +9 -1
- data/lib/atomic_cache/atomic_cache_client.rb +14 -6
- data/lib/atomic_cache/key/last_mod_time_key_manager.rb +8 -2
- data/lib/atomic_cache/storage/memory.rb +8 -3
- data/lib/atomic_cache/storage/shared_memory.rb +15 -0
- data/lib/atomic_cache/version.rb +1 -1
- data/spec/atomic_cache/atomic_cache_client_spec.rb +20 -7
- data/spec/atomic_cache/key/last_mod_time_key_manager_spec.rb +9 -0
- data/spec/atomic_cache/storage/memory_spec.rb +20 -5
- data/spec/atomic_cache/storage/shared_memory_spec.rb +15 -1
- data/spec/spec_helper.rb +4 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 47216ee7b28e97d5a2a0d45ec7a7831d6ee05038f2029498654006b7ad42171c
|
4
|
+
data.tar.gz: 025c598a7d72bd3387c0fe0057a3047227ad9c6ae67ed818912a98d3a3f21e05
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d575cb83f4bd7a97d902ad8b15d2da5063ea12edfcadc4d4b6c21f9ea1600413635cb84ee98025747bf49a82a7f97a91a73a5ba6152d7c5095ce19b556c5b7ad
|
7
|
+
data.tar.gz: 19486f04dfeb751f70b8e1c828b6affa6619829f2a467b87070b4c81a38d7656a57d7ceb02d935d1cf33e3bf4fc47f1356769cb27a19ae177a73cd0af86dbb6b
|
data/docs/USAGE.md
CHANGED
@@ -72,6 +72,13 @@ All incoming keys are normalized to symbols. All values are stored with a `valu
|
|
72
72
|
|
73
73
|
It's likely preferable to use an environments file to configure the `key_storage` and `cache_storage` to always be an in-memory adapter when running in the test environment instead of manually configuring the storage adapter per spec.
|
74
74
|
|
75
|
+
#### TTL in Tests
|
76
|
+
In a test environment, unlike in a production environment, database queries are fast, and time doesn't elapse quite like it does in the real world. As tests get more complex, they perform changes for which they expect the cache to expire. However, because of the synthetic nature of testing, TTLs, particularly those on locks, don't quite work the same either.
|
77
|
+
|
78
|
+
There are a few approaches to address this, for example, using `sleep` to cause real time to pass (not preferable) or wrapping each test in a TimeCop, forcing time to pass (works but quite manual).
|
79
|
+
|
80
|
+
Since this situation is highly likely to arise, `atomic_cache` provides a feature to globally disable enforcing TTL on locks for the `SharedMemory` implementation. Set `enforce_ttl = false` to disable TTL checking on locks within SharedMemory in a test context. This will prevent tests from failing due to unexpired TTLs on locks.
|
81
|
+
|
75
82
|
#### ★ Testing Tip ★
|
76
83
|
If using `SharedMemory` for integration style tests, a global `before(:each)` can be configured in `spec_helper.rb`.
|
77
84
|
|
@@ -79,9 +86,10 @@ If using `SharedMemory` for integration style tests, a global `before(:each)` ca
|
|
79
86
|
# spec/spec_helper.rb
|
80
87
|
RSpec.configure do |config|
|
81
88
|
|
82
|
-
#your other config
|
89
|
+
# your other config
|
83
90
|
|
84
91
|
config.before(:each) do
|
92
|
+
AtomicCache::Storage::SharedMemory.enforce_ttl = false
|
85
93
|
AtomicCache::Storage::SharedMemory.reset
|
86
94
|
end
|
87
95
|
end
|
@@ -93,8 +93,8 @@ module AtomicCache
|
|
93
93
|
end
|
94
94
|
|
95
95
|
new_key = @timestamp_manager.next_key(keyspace, lmt)
|
96
|
-
@timestamp_manager.promote(keyspace, last_known_key: new_key, timestamp: lmt)
|
97
96
|
@storage.set(new_key, new_value, options)
|
97
|
+
@timestamp_manager.promote(keyspace, last_known_key: new_key, timestamp: lmt)
|
98
98
|
|
99
99
|
metrics(:increment, 'generate.current-thread', tags: tags)
|
100
100
|
log(:debug, "Generating new value for `#{new_key}`")
|
@@ -119,13 +119,11 @@ module AtomicCache
|
|
119
119
|
return lkv
|
120
120
|
end
|
121
121
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
@storage.delete(lkk)
|
122
|
+
metrics(:increment, 'last-known-value.nil', tags: tags)
|
123
|
+
else
|
124
|
+
metrics(:increment, 'last-known-value.not-present', tags: tags)
|
126
125
|
end
|
127
126
|
|
128
|
-
metrics(:increment, 'last-known-value.not-present', tags: tags)
|
129
127
|
nil
|
130
128
|
end
|
131
129
|
|
@@ -146,6 +144,16 @@ module AtomicCache
|
|
146
144
|
if !value.nil?
|
147
145
|
metrics(:increment, 'wait.present', tags: metrics_tags)
|
148
146
|
return value
|
147
|
+
else
|
148
|
+
# if we didn't get a fresh value this go-round, check if there's a last known value
|
149
|
+
# if expirations were to come in rapidly, it's possible that the expiration which caused
|
150
|
+
# the wait cycle wrote a value, and it's now in LKV, and a new expiration came in, which
|
151
|
+
# has moved the LMT forward
|
152
|
+
value = last_known_value(keyspace, options, tags)
|
153
|
+
if !value.nil?
|
154
|
+
metrics(:increment, 'wait.lkv.present', tags: metrics_tags)
|
155
|
+
return value
|
156
|
+
end
|
149
157
|
end
|
150
158
|
end
|
151
159
|
|
@@ -44,8 +44,7 @@ module AtomicCache
|
|
44
44
|
# @param last_known_key [String] a key with a known value to refer other processes to
|
45
45
|
# @param timestamp [String, Numeric, Time] the timestamp with which the last_known_key was updated at
|
46
46
|
def promote(keyspace, last_known_key:, timestamp:)
|
47
|
-
|
48
|
-
@storage.set(key, last_known_key)
|
47
|
+
@storage.set(keyspace.last_known_key_key, last_known_key)
|
49
48
|
@storage.set(last_modified_time_key, self.format(timestamp))
|
50
49
|
end
|
51
50
|
|
@@ -59,6 +58,13 @@ module AtomicCache
|
|
59
58
|
@storage.add(keyspace.lock_key, LOCK_VALUE, ttl, options)
|
60
59
|
end
|
61
60
|
|
61
|
+
# check if the keyspace is locked
|
62
|
+
#
|
63
|
+
# @param keyspace [AtomicCache::Keyspace] keyspace to lock
|
64
|
+
def lock_present?(keyspace)
|
65
|
+
@storage.read(keyspace.lock_key) == LOCK_VALUE
|
66
|
+
end
|
67
|
+
|
62
68
|
# remove existing lock to allow other processes to update keyspace
|
63
69
|
#
|
64
70
|
# @param keyspace [AtomicCache::Keyspace] keyspace to lock
|
@@ -16,7 +16,7 @@ module AtomicCache
|
|
16
16
|
|
17
17
|
def add(raw_key, new_value, ttl, user_options={})
|
18
18
|
store_op(raw_key, user_options) do |key, options|
|
19
|
-
return false if store.has_key?(key)
|
19
|
+
return false if store.has_key?(key) && !ttl_expired?(store[key])
|
20
20
|
write(key, new_value, ttl, user_options)
|
21
21
|
end
|
22
22
|
end
|
@@ -29,8 +29,7 @@ module AtomicCache
|
|
29
29
|
unmarshaled = unmarshal(entry[:value], user_options)
|
30
30
|
return unmarshaled if entry[:ttl].nil? or entry[:ttl] == false
|
31
31
|
|
32
|
-
|
33
|
-
if (life >= entry[:ttl])
|
32
|
+
if ttl_expired?(entry)
|
34
33
|
store.delete(key)
|
35
34
|
nil
|
36
35
|
else
|
@@ -54,6 +53,12 @@ module AtomicCache
|
|
54
53
|
|
55
54
|
protected
|
56
55
|
|
56
|
+
def ttl_expired?(entry)
|
57
|
+
return false unless entry
|
58
|
+
life = Time.now - entry[:written_at]
|
59
|
+
life >= entry[:ttl]
|
60
|
+
end
|
61
|
+
|
57
62
|
def write(key, value, ttl=nil, user_options)
|
58
63
|
store[key] = {
|
59
64
|
value: marshal(value, user_options),
|
@@ -10,6 +10,21 @@ module AtomicCache
|
|
10
10
|
STORE = {}
|
11
11
|
SEMAPHORE = Mutex.new
|
12
12
|
|
13
|
+
@enforce_ttl = true
|
14
|
+
class << self
|
15
|
+
attr_accessor :enforce_ttl
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(raw_key, new_value, ttl, user_options={})
|
19
|
+
if self.class.enforce_ttl
|
20
|
+
super(raw_key, new_value, ttl, user_options)
|
21
|
+
else
|
22
|
+
store_op(raw_key, user_options) do |key, options|
|
23
|
+
write(key, new_value, ttl, user_options)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
13
28
|
def self.reset
|
14
29
|
STORE.clear
|
15
30
|
end
|
data/lib/atomic_cache/version.rb
CHANGED
@@ -163,18 +163,31 @@ describe 'AtomicCacheClient' do
|
|
163
163
|
expect(result).to eq(new_value)
|
164
164
|
end
|
165
165
|
|
166
|
+
it 'uses the last known value if the LMT increments while waiting' do
|
167
|
+
key_storage.set(timestamp_manager.last_modified_time_key, '1420090000')
|
168
|
+
key_storage.set(keyspace.last_known_key_key, 'lkk_key')
|
169
|
+
last_known_value = 'value from another thread'
|
170
|
+
|
171
|
+
# fetching the 'fresh' value continually returns nil (because LMT is incrementing forward)
|
172
|
+
allow(cache_storage).to receive(:read)
|
173
|
+
.with(timestamp_manager.current_key(keyspace), anything)
|
174
|
+
.and_return(nil, nil, nil, nil)
|
175
|
+
|
176
|
+
# multiple returned values are faking what it would look like if another process
|
177
|
+
# promoted a value (wrote LKV) but then the cache expired right after
|
178
|
+
allow(cache_storage).to receive(:read)
|
179
|
+
.with(timestamp_manager.last_known_key(keyspace), anything)
|
180
|
+
.and_return(nil, nil, nil, last_known_value)
|
181
|
+
|
182
|
+
result = subject.fetch(keyspace, backoff_duration_ms: 5) { 'value from generate' }
|
183
|
+
expect(result).to eq(last_known_value)
|
184
|
+
end
|
185
|
+
|
166
186
|
it 'stops waiting when the max retry count is reached' do
|
167
187
|
timestamp_manager.promote(keyspace, last_known_key: 'asdf', timestamp: 1420090000)
|
168
188
|
result = subject.fetch(keyspace, backoff_duration_ms: 5) { 'value from generate' }
|
169
189
|
expect(result).to eq(nil)
|
170
190
|
end
|
171
|
-
|
172
|
-
it 'deletes the last known key' do
|
173
|
-
key_storage.set(keyspace.last_known_key_key, :oldkey)
|
174
|
-
cache_storage.set(:oldkey, nil)
|
175
|
-
subject.fetch(keyspace, backoff_duration_ms: 5) { 'value from generate' }
|
176
|
-
expect(cache_storage.store).to_not have_key(:oldkey)
|
177
|
-
end
|
178
191
|
end
|
179
192
|
end
|
180
193
|
end
|
@@ -40,6 +40,15 @@ describe 'LastModTimeKeyManager' do
|
|
40
40
|
expect(storage.store).to_not have_key(:'ns:lock')
|
41
41
|
end
|
42
42
|
|
43
|
+
it 'checks if the lock is present' do
|
44
|
+
subject.lock(req_keyspace, 100)
|
45
|
+
expect(subject.lock_present?(req_keyspace)).to eq(true)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'checks if the lock is not present' do
|
49
|
+
expect(subject.lock_present?(req_keyspace)).to eq(false)
|
50
|
+
end
|
51
|
+
|
43
52
|
it 'promotes a timestamp and last known key' do
|
44
53
|
subject.promote(req_keyspace, last_known_key: 'asdf', timestamp: timestamp)
|
45
54
|
expect(storage.read(:'ns:lkk')).to eq('asdf')
|
@@ -17,17 +17,32 @@ shared_examples 'memory storage' do
|
|
17
17
|
expect(result).to eq(true)
|
18
18
|
end
|
19
19
|
|
20
|
-
it 'does not write the key if it exists' do
|
21
|
-
entry = { value: Marshal.dump('foo'), ttl:
|
20
|
+
it 'does not write the key if it exists but expiration time is NOT up' do
|
21
|
+
entry = { value: Marshal.dump('foo'), ttl: 5000, written_at: Time.local(2021, 1, 1, 12, 0, 0) }
|
22
22
|
subject.store[:key] = entry
|
23
23
|
|
24
|
-
|
25
|
-
|
24
|
+
Timecop.freeze(Time.local(2021, 1, 1, 12, 0, 1)) do
|
25
|
+
result = subject.add('key', 'value', 5000)
|
26
|
+
expect(result).to eq(false)
|
27
|
+
end
|
26
28
|
|
27
29
|
# stored values should not have changed
|
28
30
|
expect(subject.store).to have_key(:key)
|
29
31
|
expect(Marshal.load(subject.store[:key][:value])).to eq('foo')
|
30
|
-
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'does write the key if it exists and expiration time IS up' do
|
35
|
+
entry = { value: Marshal.dump('foo'), ttl: 50, written_at: Time.local(2021, 1, 1, 12, 0, 0) }
|
36
|
+
subject.store[:key] = entry
|
37
|
+
|
38
|
+
Timecop.freeze(Time.local(2021, 1, 1, 12, 30, 0)) do
|
39
|
+
result = subject.add('key', 'value', 50)
|
40
|
+
expect(result).to eq(true)
|
41
|
+
end
|
42
|
+
|
43
|
+
# stored values should not have changed
|
44
|
+
expect(subject.store).to have_key(:key)
|
45
|
+
expect(Marshal.load(subject.store[:key][:value])).to eq('value')
|
31
46
|
end
|
32
47
|
end
|
33
48
|
|
@@ -3,7 +3,21 @@
|
|
3
3
|
require 'spec_helper'
|
4
4
|
require_relative 'memory_spec'
|
5
5
|
|
6
|
-
describe '
|
6
|
+
describe 'SharedMemory' do
|
7
7
|
subject { AtomicCache::Storage::SharedMemory.new }
|
8
8
|
it_behaves_like 'memory storage'
|
9
|
+
|
10
|
+
context 'enforce_ttl disabled' do
|
11
|
+
before(:each) do
|
12
|
+
AtomicCache::Storage::SharedMemory.enforce_ttl = false
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'allows instantly `add`ing keys' do
|
16
|
+
subject.add("foo", 1, ttl: 100000)
|
17
|
+
subject.add("foo", 2, ttl: 1)
|
18
|
+
|
19
|
+
expect(subject.store).to have_key(:foo)
|
20
|
+
expect(Marshal.load(subject.store[:foo][:value])).to eq(2)
|
21
|
+
end
|
22
|
+
end
|
9
23
|
end
|
data/spec/spec_helper.rb
CHANGED
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.
|
4
|
+
version: 0.5.2.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-07-
|
12
|
+
date: 2021-07-13 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|