atomic_cache 0.2.1.rc1 → 0.2.5.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: 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: []