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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f8d7906f63f08bf7e8d7b9f9f88d05a209d65c37375c464f60448ffd9db427e
4
- data.tar.gz: ac2b7c09f3299ccbb4dcf588cbe163134c8e81b94d79c08fe908afb6200911a7
3
+ metadata.gz: 47216ee7b28e97d5a2a0d45ec7a7831d6ee05038f2029498654006b7ad42171c
4
+ data.tar.gz: 025c598a7d72bd3387c0fe0057a3047227ad9c6ae67ed818912a98d3a3f21e05
5
5
  SHA512:
6
- metadata.gz: fa4f82e7cd8b729461b5b122a9f9cb92c2c36898c295a63bdd18a510133920ca5694d3e44b9cceceb7d5e2a078b2781f75481b32bda1e21804f720a00287eacb
7
- data.tar.gz: 18ca04880e1f4c57308d3442fa4bbb50318121ba9d663ca2858819838ddcd27c118ad79b4767c60708872778deca668b7315f7cad32fdcb951cec2a18dc168ac
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
- # if the value of the last known key is nil, we can infer that it's
123
- # most likely expired, thus remove it so other processes don't waste
124
- # time trying to read it
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
- key = keyspace.last_known_key_key
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
- life = Time.now - entry[:written_at]
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicCache
4
- VERSION = "0.4.0.rc1"
4
+ VERSION = "0.5.2.rc1"
5
5
  end
@@ -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: 100, written_at: 100 }
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
- result = subject.add('key', 'value', 200)
25
- expect(result).to eq(false)
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
- expect(subject.store[:key][:ttl]).to eq(100)
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 'InstanceMemory' do
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
@@ -17,4 +17,8 @@ RSpec.configure do |config|
17
17
  expectations.include_chain_clauses_in_custom_matcher_descriptions = true
18
18
  expectations.syntax = :expect
19
19
  end
20
+
21
+ config.before(:each) do
22
+ AtomicCache::Storage::SharedMemory.enforce_ttl = true
23
+ end
20
24
  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.4.0.rc1
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-07 00:00:00.000000000 Z
12
+ date: 2021-07-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler