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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c6587484871a77f213f41c540019fbb9e945b8eb5d759bbab2c535e1c1ba58ea
4
- data.tar.gz: c1c4377f19df9b28321e5fb6214c8c687bcc9c6857d5b6843663aa068209b932
3
+ metadata.gz: 5c6e0bf96718fb99b6047009b67c12f3fb4da0b36fd18ba8eb7aded71b744cb2
4
+ data.tar.gz: 8443307bb59ff3e3ca393cb6401f6c30974ec97bc3729024848be74504c5abda
5
5
  SHA512:
6
- metadata.gz: fe10e185b34e6b754329c96c37268fa2a66a81a768c0d8c00c32d462034d0eb4f55c709c7356c537c4fb1f5a0a053412f618c73896876da8b20f0279478dadc8
7
- data.tar.gz: 1ab35be3c84aca48ea62194a3ec06e7481681989a36866304e4fa70f9a48eeb54d08c12ba344a0c85afc5f6a1773009ef47f414209c628d18d0956dd78297c80
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(keyspace, options, tags) || last_known_value(keyspace, options, tags)
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(key, options, tags)
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(keyspace, options, tags)
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(key, options, tags)
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 fetching cache key `#{key}`. Exceeded max retries (#{max_retries}).")
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
- response = @dalli_client.add(key, new_value, ttl, opts)
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.read(key, user_options)
30
+ @dalli_client.get(key, user_options)
36
31
  end
37
32
 
38
33
  def set(key, value, user_options={})
39
- @dalli_client.set(key, value, user_options)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicCache
4
- VERSION = "0.2.1.rc1"
4
+ VERSION = "0.2.5.rc1"
5
5
  end
@@ -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
- it 'delegates #set without options' do
17
- expect(dalli_client).to receive(:set).with('key', 'value', {})
18
- subject.set('key', 'value')
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(:read).with('key', {}).and_return('asdf')
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('NOT_STORED\r\n')
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('STORED\r\n')
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('EXISTS\r\n')
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.1.rc1
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-09 00:00:00.000000000 Z
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: desc
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: 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: []