atomic_cache 0.4.1.rc1 → 0.5.3.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: db2b157ed872e6cb90af8cf2ef61a97f237485bc763998464eb4faf488d26c11
4
- data.tar.gz: 48e13e212479454f2ece192c81c8f755a4aa039a2cf5930cf1cdfea2eaae438d
3
+ metadata.gz: 508ff6dd988d3b864438c2c54d3bca4cba2490a62c2ae97ed2b313522d5e8694
4
+ data.tar.gz: e0cd9a214c089780758955d2642c406484e0404c669d87faf347de1d8cb3a781
5
5
  SHA512:
6
- metadata.gz: 5fb9aba2eaeb20e9e3206c80039941b005c7f6e82c05bfddd59482783b028706f706a6843a8ea8261d18895732035d1deeabda178e5437620f33ca9abf88b54e
7
- data.tar.gz: 06511e8a0ec9b33de005994f6938877f85b053c03072f52b6cc0dbd22a99389dfb36245f7002d59ea364d1ea583eec0b440c6bd2a5e1b84604434474275d6fe2
6
+ metadata.gz: f5e751211a50a029cf4043fa89bf74e50cb200835985c884b5a0faa782b9fc3b8b4c9a0b55be561763363c6625e124c9c11bee75cb15596ddea7b7213bb20bc7
7
+ data.tar.gz: b3fa0f5bed38fe417103a02409fcb1bf9ccb7611d69887ac6b5cc565e1440a6148ab9f306cb3283fbce2369bf26012f2532a21e889871a47e22d3a517f8b9b84
data/docs/USAGE.md CHANGED
@@ -38,6 +38,12 @@ The ideal `generate_ttl_ms` time is just slightly longer than the average genera
38
38
 
39
39
  If metrics are enabled, the `<namespace>.generate.run` can be used to determine the min/max/average generate time for a particular cache and the `generate_ttl_ms` tuned using that.
40
40
 
41
+ ##### ⚠️ TTL Rounding
42
+ When using atomic_cache with memcached, be aware that the TTL will be rounded down to the nearest whole seconds. For example, a `generate_ttl_ms` value of 3500 will result in a 3s TTL with memcache.
43
+
44
+ ##### ⚠️ Max Rate of Change
45
+ Atomic_cache will *not* remove the lock after the generation process is done. This is both more efficient, and allows the `generate_ttl_ms` to be used to limit the total rate of change. For example, if the typical database query behind the cache takes 2s to run, but `generate_ttl_ms` is set to 10s, then the lock will live on for 8s after the generate process finishes, preventing other processes from querying for a new value. In many ways, `generate_ttl_ms` is the amount of time that the system will be un-allowed to make additional queries.
46
+
41
47
  #### `max_retries` & `backoff_duration_ms`
42
48
  _`max_retries` defaults to 5._
43
49
  _`backoff_duration_ms` defaults to 50ms._
@@ -72,6 +78,13 @@ All incoming keys are normalized to symbols. All values are stored with a `valu
72
78
 
73
79
  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
80
 
81
+ #### TTL in Tests
82
+ 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.
83
+
84
+ 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).
85
+
86
+ 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.
87
+
75
88
  #### ★ Testing Tip ★
76
89
  If using `SharedMemory` for integration style tests, a global `before(:each)` can be configured in `spec_helper.rb`.
77
90
 
@@ -79,9 +92,10 @@ If using `SharedMemory` for integration style tests, a global `before(:each)` ca
79
92
  # spec/spec_helper.rb
80
93
  RSpec.configure do |config|
81
94
 
82
- #your other config
95
+ # your other config
83
96
 
84
97
  config.before(:each) do
98
+ AtomicCache::Storage::SharedMemory.enforce_ttl = false
85
99
  AtomicCache::Storage::SharedMemory.reset
86
100
  end
87
101
  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
 
@@ -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
@@ -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.1.rc1"
4
+ VERSION = "0.5.3.rc1"
5
5
  end
@@ -168,13 +168,6 @@ describe 'AtomicCacheClient' do
168
168
  result = subject.fetch(keyspace, backoff_duration_ms: 5) { 'value from generate' }
169
169
  expect(result).to eq(nil)
170
170
  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
171
  end
179
172
  end
180
173
  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,8 +17,6 @@ shared_examples 'memory storage' do
17
17
  expect(result).to eq(true)
18
18
  end
19
19
 
20
- # SharedMemory.new.add("foo", ttl: 100)
21
-
22
20
  it 'does not write the key if it exists but expiration time is NOT up' do
23
21
  entry = { value: Marshal.dump('foo'), ttl: 5000, written_at: Time.local(2021, 1, 1, 12, 0, 0) }
24
22
  subject.store[:key] = entry
@@ -6,4 +6,18 @@ require_relative 'memory_spec'
6
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.1.rc1
4
+ version: 0.5.3.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ibotta Developers
@@ -9,22 +9,22 @@ 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-08-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
16
16
  requirement: !ruby/object:Gem::Requirement
17
17
  requirements:
18
- - - "~>"
18
+ - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: '1.14'
20
+ version: '0'
21
21
  type: :development
22
22
  prerelease: false
23
23
  version_requirements: !ruby/object:Gem::Requirement
24
24
  requirements:
25
- - - "~>"
25
+ - - ">="
26
26
  - !ruby/object:Gem::Version
27
- version: '1.14'
27
+ version: '0'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: gems
30
30
  requirement: !ruby/object:Gem::Requirement
@@ -144,9 +144,6 @@ dependencies:
144
144
  - - ">="
145
145
  - !ruby/object:Gem::Version
146
146
  version: '4.2'
147
- - - "<"
148
- - !ruby/object:Gem::Version
149
- version: '6'
150
147
  type: :runtime
151
148
  prerelease: false
152
149
  version_requirements: !ruby/object:Gem::Requirement
@@ -154,9 +151,6 @@ dependencies:
154
151
  - - ">="
155
152
  - !ruby/object:Gem::Version
156
153
  version: '4.2'
157
- - - "<"
158
- - !ruby/object:Gem::Version
159
- version: '6'
160
154
  - !ruby/object:Gem::Dependency
161
155
  name: murmurhash3
162
156
  requirement: !ruby/object:Gem::Requirement