atomic_cache 0.1.0.rc1 → 0.1.0.rc2

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
- SHA1:
3
- metadata.gz: a6de69eb6ef0c02cbe647a36487ee3e5ef051c63
4
- data.tar.gz: 140e5dacb3a85b7e67259b9789599f8d73c9d9d0
2
+ SHA256:
3
+ metadata.gz: 86d21873e0440ffb327b3d9e6de724ffff524d7075c5f4588e4a4c46042cf47a
4
+ data.tar.gz: 956391de071f7997ccd078157449634436e3ed6316215bf6319e830c979fe0d0
5
5
  SHA512:
6
- metadata.gz: 604b436749bba999493381af064d0011a6cd5197bc191267873415370e8f8484e523780ca89ad5ed302ab40bf96b784f6df505bb0ffb14a40a72e17377f049ca
7
- data.tar.gz: 3abb13a75e2fb3f426ac8ea9907ca7591692f7e8219b3516009522da6f5a044fe75a360a5ac876d68c45c26e8842f6326a1a90d5b38dbd4ac1bff1b3bc23cb75
6
+ metadata.gz: 72eca9fdf5c4018f8934917219abb4ccde4cba7fae17482550b2087673eacec18cae5e5cb6686a00fb317424bae379ddb83cfc50f767ae456b936ac71fe8a702
7
+ data.tar.gz: 4739607a81c343ba2b73de9f7e36d18d4c1a76969f51d7ab4c3cbbdf54fb3274e6bc9d0244a75e8b4a8fada4aea59aa0ce5b05ed397085a5ef606fe801afd233
data/README.md CHANGED
@@ -1,4 +1,5 @@
1
1
  # atomic_cache Gem
2
+ [![Gem Version](https://badge.fury.io/rb/atomic_cache.svg)](https://badge.fury.io/rb/atomic_cache)
2
3
  [![Build Status](https://travis-ci.org/Ibotta/atomic_cache.svg?branch=master)](https://travis-ci.org/Ibotta/atomic_cache)
3
4
  [![Test Coverage](https://api.codeclimate.com/v1/badges/790faad5866d2a00ca6c/test_coverage)](https://codeclimate.com/github/Ibotta/atomic_cache/test_coverage)
4
5
 
@@ -42,8 +43,15 @@ For further details and examples see [Usage & Testing](docs/USAGE.md)
42
43
 
43
44
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
44
45
 
45
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
46
-
47
46
  ## Contributing
48
47
 
49
48
  Bug reports and pull requests are welcome on GitHub at https://github.com/ibotta/atomic_cache
49
+
50
+ ## Releasing
51
+
52
+ Releases are automatically handled via the Travis CI build. When a version greater than
53
+ the version published on rubygems.org is pushed to the `master` branch, Travis will:
54
+
55
+ - re-generate the CHANGELOG file
56
+ - tag the release with GitHub
57
+ - release to rubygems.org
@@ -1,7 +1,4 @@
1
1
  ## Gem Installation
2
-
3
- You will need to ensure you have the correct deploy credentials
4
-
5
2
  Add this line to your application's Gemfile:
6
3
 
7
4
  ```ruby
@@ -28,6 +25,8 @@ AtomicCache::DefaultConfig.configure do |config|
28
25
  end
29
26
  ```
30
27
 
28
+ Note that `Datadog::Statsd` is not _required_. Adding it, however, will enable metrics support.
29
+
31
30
  #### Required
32
31
  * `cache_storage` - Storage adapter for cache (see below)
33
32
  * `key_storage` - Storage adapter for key manager (see below)
data/docs/USAGE.md CHANGED
@@ -39,7 +39,7 @@ The danger with `quick_retry_ms` is that when enabled it applies a delay to all
39
39
 
40
40
  `quick_retry_ms` is most effective for caches that are quick to generate but whose values are slow to change. `quick_retry_ms` is least effective for caches that are slow to update but quick to change.
41
41
 
42
- ![quick_retry_ms graph](img/quick_retry_ms_graph.png)
42
+ ![quick_retry_ms graph](https://github.com/Ibotta/atomic_cache/raw/ca473f28e179da8c24f638eeeeb48750bc8cbe64/docs/img/quick_retry_graph.png)
43
43
 
44
44
  #### `max_retries` & `backoff_duration_ms`
45
45
  _`max_retries` defaults to 5._
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicCache
4
- VERSION = "0.1.0.rc1"
4
+ VERSION = "0.1.0.rc2"
5
5
  end
@@ -0,0 +1,213 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'AtomicCacheClient' do
6
+ subject { AtomicCache::AtomicCacheClient.new(storage: cache_storage, timestamp_manager: timestamp_manager) }
7
+
8
+ let(:formatter) { Proc.new { |time| time.to_i } }
9
+ let(:keyspace) { AtomicCache::Keyspace.new(namespace: ['foo', 'bar'], root: 'bar') }
10
+ let(:key_storage) { AtomicCache::Storage::InstanceMemory.new }
11
+ let(:cache_storage) { AtomicCache::Storage::InstanceMemory.new }
12
+
13
+ let(:timestamp_manager) do
14
+ AtomicCache::LastModTimeKeyManager.new(
15
+ keyspace: keyspace,
16
+ storage: key_storage,
17
+ timestamp_formatter: formatter,
18
+ )
19
+ end
20
+
21
+ before(:each) do
22
+ AtomicCache::DefaultConfig.reset
23
+ end
24
+
25
+ describe '#fetch' do
26
+
27
+ context 'when the value is present' do
28
+ before(:each) do
29
+ timestamp_manager.last_modified_time = 1420090000
30
+ end
31
+
32
+ it 'returns the cached value' do
33
+ cache_storage.set(timestamp_manager.current_key(keyspace), 'value')
34
+ expect(subject.fetch(keyspace)).to eq('value')
35
+ end
36
+
37
+ it 'returns 0 as a cached value' do
38
+ cache_storage.set(timestamp_manager.current_key(keyspace), '0')
39
+ expect(subject.fetch(keyspace)).to eq('0')
40
+ end
41
+
42
+ it 'returns empty strings as a cached value' do
43
+ cache_storage.set(timestamp_manager.current_key(keyspace), '')
44
+ expect(subject.fetch(keyspace)).to eq('')
45
+ end
46
+ end
47
+
48
+ context 'when the value is NOT present' do
49
+ context 'and when a block is given' do
50
+ context 'and when another thread is NOT generating,' do
51
+
52
+ it 'returns the new value' do
53
+ result = subject.fetch(keyspace) { 'value from block' }
54
+ expect(result).to eq('value from block')
55
+ end
56
+
57
+ it 'returns the new value when it is an empty string' do
58
+ result = subject.fetch(keyspace) { '' }
59
+ expect(result).to eq('')
60
+ end
61
+
62
+ it 'does not store the value if the generator returns nil' do
63
+ # create a fallback value to make sure we don't use the value from the block
64
+ key_storage.set(keyspace.last_known_key_key, 'foo_value')
65
+ cache_storage.set('foo', 'last known value')
66
+
67
+ timestamp_manager.promote(keyspace, last_known_key: 'foo', timestamp: Time.now)
68
+ subject.fetch(keyspace) { nil }
69
+ expect(subject.fetch(keyspace)).to eq('last known value')
70
+ end
71
+
72
+ it 'unlocks if the generate block returns nil' do
73
+ subject.fetch(keyspace) { nil }
74
+ expect(key_storage.store).to_not have_key(:'foo:bar:lock')
75
+ end
76
+
77
+ it 'stores the new value' do
78
+ subject.fetch(keyspace) { 'value from block' }
79
+ expect(subject.fetch(keyspace)).to eq('value from block')
80
+ end
81
+
82
+ it 'stores the updated last mod time' do
83
+ time = Time.local(2018, 1, 1, 15, 30, 0)
84
+ timestamp_manager.promote(keyspace, timestamp: (time - 10).to_i, last_known_key: 'lkk')
85
+
86
+ Timecop.freeze(time) do
87
+ subject.fetch(keyspace) { 'value from block' }
88
+ lmt = key_storage.read(timestamp_manager.last_modified_time_key)
89
+ expect(lmt).to eq(time.to_i.to_s)
90
+ end
91
+ end
92
+
93
+ it 'stores the current key as the last known key' do
94
+ time = Time.local(2018, 1, 1, 15, 30, 0)
95
+ timestamp_manager.promote(keyspace, last_known_key: "test:#{(time - 10).to_i}", timestamp: time.to_i)
96
+
97
+ Timecop.freeze(time) do
98
+ subject.fetch(keyspace) { 'value from block' }
99
+ lkk = key_storage.read(keyspace.last_known_key_key)
100
+ new_key = timestamp_manager.next_key(keyspace, time)
101
+ expect(lkk).to eq(new_key)
102
+ end
103
+ end
104
+
105
+ it 'sets a TTL on the build key when a TTL is not explicitly given' do
106
+ subject.fetch(keyspace) { 'value from block' }
107
+ lock_entry = key_storage.store[keyspace.lock_key.to_sym]
108
+ expect(lock_entry[:ttl]).to eq(30)
109
+ end
110
+
111
+ it 'sets a TTL on the build key when a TTL is given at fetch time' do
112
+ subject.fetch(keyspace, generate_ttl_ms: 1100) { 'value from block' }
113
+ lock_entry = key_storage.store[keyspace.lock_key.to_sym]
114
+ expect(lock_entry[:ttl]).to eq(1.1)
115
+ end
116
+
117
+ it 'sets a TTL on the build key when a value less than a second is given' do
118
+ subject.fetch(keyspace, generate_ttl_ms: 500) { 'value from block' }
119
+ lock_entry = key_storage.store[keyspace.lock_key.to_sym]
120
+ expect(lock_entry[:ttl]).to eq(0.5)
121
+ end
122
+
123
+ it 'sets a TTL on the build key when there is a TTL in the default options' do
124
+ subject = AtomicCache::AtomicCacheClient.new(
125
+ storage: cache_storage,
126
+ timestamp_manager: timestamp_manager,
127
+ default_options: { generate_ttl_ms: 600 }
128
+ )
129
+
130
+ subject.fetch(keyspace) { 'value from block' }
131
+ lock_entry = key_storage.store[keyspace.lock_key.to_sym]
132
+ expect(lock_entry[:ttl]).to eq(0.6)
133
+ end
134
+ end
135
+
136
+ context 'and when another thread is generating the new value,' do
137
+ before(:each) do
138
+ timestamp_manager.lock(keyspace, 100)
139
+ end
140
+
141
+ it 'waits for a short duration to see if the other thread generated the value' do
142
+ timestamp_manager.promote(keyspace, last_known_key: 'lkk', timestamp: 1420090000)
143
+ key_storage.set('lkk', 'old:value')
144
+ new_value = 'value from another thread'
145
+ allow(cache_storage).to receive(:read)
146
+ .with(timestamp_manager.current_key(keyspace), anything)
147
+ .and_return(nil, new_value)
148
+
149
+ expect(subject.fetch(keyspace, quick_retry_ms: 5) { 'value' }).to eq(new_value)
150
+ end
151
+
152
+ context 'when the last known value is present' do
153
+ it 'returns the last known value' do
154
+ timestamp_manager.promote(keyspace, last_known_key: 'lkk', timestamp: 1420090000)
155
+ cache_storage.set('lkk', 'old value')
156
+
157
+ result = subject.fetch(keyspace, backoff_duration_ms: 5) { 'value from generate' }
158
+ expect(result).to eq('old value')
159
+ end
160
+ end
161
+
162
+ context 'when the last known value is NOT present' do
163
+ it 'waits for another thread to generate the new value' do
164
+ key_storage.set(timestamp_manager.last_modified_time_key, '1420090000')
165
+ new_value = 'value from another thread'
166
+
167
+ # multiple returned values here are faking what it would look like to
168
+ # the client if another thread suddenly wrote a value into the cache
169
+ allow(cache_storage).to receive(:read)
170
+ .with(timestamp_manager.current_key(keyspace), anything)
171
+ .and_return(nil, nil, nil, nil, new_value)
172
+
173
+ result = subject.fetch(keyspace, backoff_duration_ms: 5) { 'value from generate' }
174
+ expect(result).to eq(new_value)
175
+ end
176
+
177
+ it 'stops waiting when the max retry count is reached' do
178
+ timestamp_manager.promote(keyspace, last_known_key: 'asdf', timestamp: 1420090000)
179
+ result = subject.fetch(keyspace, backoff_duration_ms: 5) { 'value from generate' }
180
+ expect(result).to eq(nil)
181
+ end
182
+
183
+ it 'deletes the last known key' do
184
+ key_storage.set(keyspace.last_known_key_key, :oldkey)
185
+ cache_storage.set(:oldkey, nil)
186
+ subject.fetch(keyspace, backoff_duration_ms: 5) { 'value from generate' }
187
+ expect(cache_storage.store).to_not have_key(:oldkey)
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ context 'and when a block is NOT given' do
194
+ it 'waits for a short duration to see if the other thread generated the value' do
195
+ timestamp_manager.promote(keyspace, last_known_key: 'asdf', timestamp: 1420090000)
196
+ new_value = 'value from another thread'
197
+ allow(cache_storage).to receive(:read)
198
+ .with(timestamp_manager.current_key(keyspace), anything)
199
+ .and_return(nil, new_value)
200
+
201
+ result = subject.fetch(keyspace, quick_retry_ms: 50)
202
+ expect(result).to eq(new_value)
203
+ end
204
+
205
+ it 'returns nil if nothing is present' do
206
+ expect(subject.fetch(keyspace)).to eq(nil)
207
+ end
208
+ end
209
+ end
210
+
211
+ end
212
+
213
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'AtomicCacheConcern' do
6
+ let(:key_storage) { DefaultConfig.instance.key_storage }
7
+ let(:cache_storage) { DefaultConfig.instance.cache_storage }
8
+
9
+ subject do
10
+ class Foo1
11
+ include AtomicCache::GlobalLMTCacheConcern
12
+ end
13
+ end
14
+
15
+ before(:context) do
16
+ DefaultConfig.instance.reset
17
+ DefaultConfig.configure do |cfg|
18
+ cfg.cache_storage = AtomicCache::Storage::SharedMemory.new
19
+ cfg.key_storage = AtomicCache::Storage::SharedMemory.new
20
+ cfg.timestamp_formatter = Proc.new { |time| time.to_i }
21
+ end
22
+ end
23
+
24
+ before(:each) do
25
+ key_storage.reset
26
+ cache_storage.reset
27
+ end
28
+
29
+ context 'AtomicCache' do
30
+ it 'initializes a cache client' do
31
+ expect(subject).to respond_to(:AtomicCache)
32
+ expect(subject.AtomicCache).to be_a(AtomicCacheClient)
33
+ expect(subject.new.AtomicCache).to be_a(AtomicCacheClient)
34
+ end
35
+
36
+ it 'uses the name of the class in the default keyspace' do
37
+ subject.expire_cache
38
+ expect(key_storage.store).to have_key(:'foo1:lmt')
39
+ end
40
+ end
41
+
42
+ context '#expire_cache' do
43
+ it 'updates the last modified time' do
44
+ time = Time.local(2018, 1, 1, 15, 30, 0)
45
+ subject.expire_cache(time)
46
+ expect(key_storage.store).to have_key(:'foo1:lmt')
47
+ expect(key_storage.store[:'foo1:lmt'][:value]).to eq(time.to_i.to_s)
48
+ end
49
+
50
+ it 'expires all the keyspaces for this class' do
51
+ old_time = Time.local(2018, 1, 1, 15, 30, 0)
52
+ new_time = Time.local(2018, 1, 1, 15, 40, 0)
53
+ ns1 = subject.cache_keyspace(:bar)
54
+ ns2 = subject.cache_keyspace(:buz)
55
+
56
+ Timecop.freeze(old_time) do
57
+ subject.AtomicCache.fetch(ns1) { 'bar' }
58
+ subject.AtomicCache.fetch(ns2) { 'buz' }
59
+ end
60
+
61
+ Timecop.freeze(new_time) do
62
+ subject.expire_cache
63
+ lmt = subject.last_modified_time
64
+
65
+ # some other process writes new values
66
+ cache_storage.set("foo1:bar:#{lmt}", 'new-bar')
67
+ cache_storage.set("foo1:buz:#{lmt}", 'new-buz')
68
+
69
+ ns1_value = subject.AtomicCache.fetch(ns1)
70
+ ns2_value = subject.AtomicCache.fetch(ns2)
71
+
72
+ expect(ns1_value).to eq('new-bar')
73
+ expect(ns2_value).to eq('new-buz')
74
+ end
75
+ end
76
+ end
77
+
78
+ context '#cache_keyspace' do
79
+ it 'returns a child keyspace of the class keyspace' do
80
+ ns = subject.cache_keyspace(:fuz, :baz)
81
+ expect(ns).to be_a(Keyspace)
82
+ expect(ns.namespace).to eq(['foo1', :fuz, :baz])
83
+ end
84
+ end
85
+
86
+ context 'keyspace macros' do
87
+ subject do
88
+ class Foo2
89
+ include AtomicCache::GlobalLMTCacheConcern
90
+ cache_version(3)
91
+ cache_class('foo')
92
+ end
93
+ Foo2
94
+ end
95
+
96
+ it 'uses the given version and cache_class become part of the cache keyspace' do
97
+ subject.expire_cache
98
+ expect(key_storage.store).to have_key(:'foo:v3:lmt')
99
+ end
100
+ end
101
+
102
+ context 'storage macros' do
103
+ subject do
104
+ class Foo3
105
+ include AtomicCache::GlobalLMTCacheConcern
106
+ cache_key_storage('keystore')
107
+ cache_value_storage('valuestore')
108
+ end
109
+ Foo3
110
+ end
111
+
112
+ it 'sets the storage for the class' do
113
+ cache_store = subject.AtomicCache.instance_variable_get(:@storage)
114
+ expect(cache_store).to eq('valuestore')
115
+
116
+ key_store = subject.instance_variable_get(:@timestamp_manager).instance_variable_get(:@storage)
117
+ expect(key_store).to eq('keystore')
118
+ end
119
+ end
120
+
121
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'DefaultConfig' do
6
+ subject { DefaultConfig }
7
+
8
+ context '#configure' do
9
+ it 'configures the singleton' do
10
+ subject.configure do |manager|
11
+ manager.namespace = 'foo'
12
+ end
13
+
14
+ expect(subject.instance.namespace).to eq('foo')
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'Keyspace' do
6
+ subject { AtomicCache::Keyspace.new(namespace: ['foo', 'bar'], root: 'foo') }
7
+
8
+ context '#initialize' do
9
+ it 'hashes non-primitive types' do
10
+ ids = [1,2,3]
11
+ ns1 = AtomicCache::Keyspace.new(namespace: ['foo', ids])
12
+ hash = ns1.send(:hexhash, ids)
13
+ expect(ns1.namespace).to eq(['foo', hash])
14
+ end
15
+
16
+ it 'leaves primitives alone' do
17
+ ns1 = AtomicCache::Keyspace.new(namespace: ['foo', :foo, 5])
18
+ expect(ns1.namespace).to eq(['foo', :foo, 5])
19
+ end
20
+
21
+ it 'sorts sortable values before hashing' do
22
+ ns1 = AtomicCache::Keyspace.new(namespace: ['foo', [1, 2, 3]])
23
+ ns2 = AtomicCache::Keyspace.new(namespace: ['foo', [3, 2, 1]])
24
+ expect(ns1.namespace).to eq(ns2.namespace)
25
+ end
26
+ end
27
+
28
+ context '#child' do
29
+ it 'extends the keyspace' do
30
+ ns2 = subject.child([:buz, :baz])
31
+ expect(ns2.namespace).to eq(['foo', 'bar', :buz, :baz])
32
+ end
33
+ end
34
+
35
+ context '#key' do
36
+ it 'return a key of the segments' do
37
+ expect(subject.key).to eq('foo:bar')
38
+ end
39
+
40
+ it 'return the key with the suffix' do
41
+ expect(subject.key('baz')).to eq('foo:bar:baz')
42
+ end
43
+ end
44
+
45
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe 'LastModTimeKeyManager' do
6
+ let(:id) { :foo }
7
+ let(:timestamp) { 1513720308 }
8
+ let(:storage) { AtomicCache::Storage::InstanceMemory.new }
9
+ let(:timestamp_keyspace) { Keyspace.new(namespace: ['ts'], root: 'foo') }
10
+ let(:req_keyspace) { Keyspace.new(namespace: ['ns'], root: 'bar') }
11
+
12
+ subject do
13
+ AtomicCache::LastModTimeKeyManager.new(
14
+ keyspace: timestamp_keyspace,
15
+ storage: storage,
16
+ timestamp_formatter: Proc.new { |t| t.to_i }
17
+ )
18
+ end
19
+
20
+ it 'returns the #next_key' do
21
+ expect(subject.next_key(req_keyspace, timestamp)).to eq('ns:1513720308')
22
+ end
23
+
24
+ it 'gets and sets the #last_known_key' do
25
+ subject.promote(req_keyspace, last_known_key: 'bar:foo:1513600308', timestamp: timestamp)
26
+ expect(subject.last_known_key(req_keyspace)).to eq('bar:foo:1513600308')
27
+ expect(storage.store).to have_key(:'ns:lkk')
28
+ end
29
+
30
+ it 'returns the #last_mod_time_key' do
31
+ expect(subject.last_modified_time_key).to eq('ts:lmt')
32
+ end
33
+
34
+ it 'locks and unlocks' do
35
+ locked = subject.lock(req_keyspace, 100)
36
+ expect(storage.store).to have_key(:'ns:lock')
37
+ expect(locked).to eq(true)
38
+
39
+ subject.unlock(req_keyspace)
40
+ expect(storage.store).to_not have_key(:'ns:lock')
41
+ end
42
+
43
+ it 'promotes a timestamp and last known key' do
44
+ subject.promote(req_keyspace, last_known_key: 'asdf', timestamp: timestamp)
45
+ expect(storage.store[:'ns:lkk'][:value]).to eq('asdf')
46
+ expect(storage.store[:'ts:lmt'][:value]).to eq(timestamp.to_s)
47
+ expect(subject.last_modified_time).to eq(timestamp.to_s)
48
+ end
49
+
50
+ context '#last_modified_time=' do
51
+ it 'returns the last modified time' do
52
+ subject.last_modified_time = timestamp
53
+ expect(storage.store[:'ts:lmt'][:value]).to eq(timestamp.to_s)
54
+ expect(subject.last_modified_time).to eq(timestamp.to_s)
55
+ end
56
+
57
+ it 'formats Time' do
58
+ now = Time.now
59
+ subject.last_modified_time = now
60
+ expect(subject.last_modified_time).to eq(now.to_i.to_s)
61
+ end
62
+ end
63
+
64
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ class FakeDalli
6
+ def add(key, new_value, ttl, user_options); end
7
+ def read(key, user_options); end
8
+ def set(key, new_value, user_options); end
9
+ def delete(key, user_options); end
10
+ end
11
+
12
+ describe 'Dalli' do
13
+ let(:dalli_client) { FakeDalli.new }
14
+ subject { AtomicCache::Storage::Dalli.new(dalli_client) }
15
+
16
+ it 'delegates #set without options' do
17
+ expect(dalli_client).to receive(:set).with('key', 'value', {})
18
+ subject.set('key', 'value')
19
+ end
20
+
21
+ it 'delegates #read without options' do
22
+ expect(dalli_client).to receive(:read).with('key', {})
23
+ subject.read('key')
24
+ end
25
+
26
+ it 'delegates #delete' do
27
+ expect(dalli_client).to receive(:delete).with('key')
28
+ subject.delete('key')
29
+ end
30
+
31
+ context '#add' do
32
+ before(:each) do
33
+ allow(dalli_client).to receive(:add).and_return('NOT_STORED\r\n')
34
+ end
35
+
36
+ it 'delegates to #add with the raw option set' do
37
+ expect(dalli_client).to receive(:add)
38
+ .with('key', 'value', 100, { foo: 'bar', raw: true })
39
+ subject.add('key', 'value', 100, { foo: 'bar' })
40
+ end
41
+
42
+ it 'returns true when the add is successful' do
43
+ expect(dalli_client).to receive(:add).and_return('STORED\r\n')
44
+ result = subject.add('key', 'value', 100)
45
+ expect(result).to eq(true)
46
+ end
47
+
48
+ it 'returns false if the key already exists' do
49
+ expect(dalli_client).to receive(:add).and_return('EXISTS\r\n')
50
+ result = subject.add('key', 'value', 100)
51
+ expect(result).to eq(false)
52
+ end
53
+
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
+ end
60
+
61
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require_relative 'memory_spec'
5
+
6
+ describe 'InstanceMemory' do
7
+ subject { AtomicCache::Storage::InstanceMemory.new }
8
+ it_behaves_like 'memory storage'
9
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ shared_examples 'memory storage' do
6
+ before(:each) do
7
+ subject.reset
8
+ end
9
+
10
+ context '#add' do
11
+ it 'writes the new key if it does not already exist' do
12
+ result = subject.add('key', 'value', 100)
13
+
14
+ expect(subject.store).to have_key(:key)
15
+ expect(subject.store[:key][:value]).to eq('value')
16
+ expect(subject.store[:key][:ttl]).to eq(100)
17
+ expect(result).to eq(true)
18
+ end
19
+
20
+ it 'does not write the key if it exists' do
21
+ entry = { value: 'foo', ttl: 100, written_at: 100 }
22
+ subject.store[:key] = entry
23
+
24
+ result = subject.add('key', 'value', 200)
25
+ expect(result).to eq(false)
26
+
27
+ # stored values should not have changed
28
+ expect(subject.store).to have_key(:key)
29
+ expect(subject.store[:key][:value]).to eq('foo')
30
+ expect(subject.store[:key][:ttl]).to eq(100)
31
+ end
32
+ end
33
+
34
+ context '#read' do
35
+ it 'returns values' do
36
+ subject.store[:sugar] = { value: 'foo' }
37
+ expect(subject.read('sugar')).to eq('foo')
38
+ end
39
+
40
+ it 'respects TTL' do
41
+ subject.store[:sugar] = { value: 'foo', ttl: 100, written_at: Time.now - 1000 }
42
+ expect(subject.read('sugar')).to eq(nil)
43
+ end
44
+ end
45
+
46
+ context '#set' do
47
+ it 'adds the value when not present' do
48
+ subject.set(:cane, 'v', expires_in: 100)
49
+ expect(subject.store).to have_key(:cane)
50
+ expect(subject.store[:cane][:value]).to eq('v')
51
+ expect(subject.store[:cane][:ttl]).to eq(100)
52
+ end
53
+
54
+ it 'overwrites existing values' do
55
+ subject.store[:cane] = { value: 'foo', ttl: 500, written_at: 500 }
56
+
57
+ subject.set(:cane, 'v', expires_in: 100)
58
+ expect(subject.store).to have_key(:cane)
59
+ expect(subject.store[:cane][:value]).to eq('v')
60
+ expect(subject.store[:cane][:ttl]).to eq(100)
61
+ end
62
+ end
63
+
64
+ context '#delete' do
65
+ it 'deletes the key' do
66
+ subject.store[:record] = { value: 'foo', written_at: 500 }
67
+ subject.delete('record')
68
+ expect(subject.store).to_not have_key(:record)
69
+ end
70
+ end
71
+
72
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require_relative 'memory_spec'
5
+
6
+ describe 'InstanceMemory' do
7
+ subject { AtomicCache::Storage::SharedMemory.new }
8
+ it_behaves_like 'memory storage'
9
+ end
@@ -0,0 +1,20 @@
1
+ require 'simplecov'
2
+ SimpleCov.start
3
+
4
+ require 'bundler/setup'
5
+ require 'atomic_cache'
6
+ require 'timecop'
7
+
8
+ DefaultConfig = AtomicCache::DefaultConfig
9
+ AtomicCacheClient = AtomicCache::AtomicCacheClient
10
+ Keyspace = AtomicCache::Keyspace
11
+
12
+ RSpec.configure do |config|
13
+ # Enable flags like --only-failures and --next-failure
14
+ config.example_status_persistence_file_path = ".rspec_status"
15
+
16
+ config.expect_with :rspec do |expectations|
17
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
18
+ expectations.syntax = :expect
19
+ end
20
+ 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.1.0.rc1
4
+ version: 0.1.0.rc2
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: 2018-02-16 00:00:00.000000000 Z
12
+ date: 2018-02-23 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -27,18 +27,60 @@ dependencies:
27
27
  version: '1.14'
28
28
  - !ruby/object:Gem::Dependency
29
29
  name: gems
30
+ requirement: !ruby/object:Gem::Requirement
31
+ requirements:
32
+ - - "~>"
33
+ - !ruby/object:Gem::Version
34
+ version: '1.0'
35
+ type: :development
36
+ prerelease: false
37
+ version_requirements: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - "~>"
40
+ - !ruby/object:Gem::Version
41
+ version: '1.0'
42
+ - !ruby/object:Gem::Dependency
43
+ name: git
44
+ requirement: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - "~>"
47
+ - !ruby/object:Gem::Version
48
+ version: '1.3'
49
+ type: :development
50
+ prerelease: false
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - "~>"
54
+ - !ruby/object:Gem::Version
55
+ version: '1.3'
56
+ - !ruby/object:Gem::Dependency
57
+ name: github_changelog_generator
30
58
  requirement: !ruby/object:Gem::Requirement
31
59
  requirements:
32
60
  - - ">="
33
61
  - !ruby/object:Gem::Version
34
- version: 1.0.0
62
+ version: 1.15.0.pre.rc
35
63
  type: :development
36
64
  prerelease: false
37
65
  version_requirements: !ruby/object:Gem::Requirement
38
66
  requirements:
39
67
  - - ">="
40
68
  - !ruby/object:Gem::Version
41
- version: 1.0.0
69
+ version: 1.15.0.pre.rc
70
+ - !ruby/object:Gem::Dependency
71
+ name: octokit
72
+ requirement: !ruby/object:Gem::Requirement
73
+ requirements:
74
+ - - "~>"
75
+ - !ruby/object:Gem::Version
76
+ version: '4.0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: !ruby/object:Gem::Requirement
80
+ requirements:
81
+ - - "~>"
82
+ - !ruby/object:Gem::Version
83
+ version: '4.0'
42
84
  - !ruby/object:Gem::Dependency
43
85
  name: rake
44
86
  requirement: !ruby/object:Gem::Requirement
@@ -102,6 +144,9 @@ dependencies:
102
144
  - - ">="
103
145
  - !ruby/object:Gem::Version
104
146
  version: '4.2'
147
+ - - "<"
148
+ - !ruby/object:Gem::Version
149
+ version: '6'
105
150
  type: :runtime
106
151
  prerelease: false
107
152
  version_requirements: !ruby/object:Gem::Requirement
@@ -109,6 +154,9 @@ dependencies:
109
154
  - - ">="
110
155
  - !ruby/object:Gem::Version
111
156
  version: '4.2'
157
+ - - "<"
158
+ - !ruby/object:Gem::Version
159
+ version: '6'
112
160
  - !ruby/object:Gem::Dependency
113
161
  name: murmurhash3
114
162
  requirement: !ruby/object:Gem::Requirement
@@ -129,17 +177,8 @@ executables: []
129
177
  extensions: []
130
178
  extra_rdoc_files: []
131
179
  files:
132
- - ".gitignore"
133
- - ".ruby_version"
134
- - ".travis.yml"
135
- - CODE_OF_CONDUCT.md
136
- - Gemfile
137
180
  - LICENSE
138
181
  - README.md
139
- - Rakefile
140
- - atomic_cache.gemspec
141
- - bin/console
142
- - bin/setup
143
182
  - docs/ARCH.md
144
183
  - docs/INTERFACES.md
145
184
  - docs/MODEL_SETUP.md
@@ -158,9 +197,19 @@ files:
158
197
  - lib/atomic_cache/storage/shared_memory.rb
159
198
  - lib/atomic_cache/storage/store.rb
160
199
  - lib/atomic_cache/version.rb
200
+ - spec/atomic_cache/atomic_cache_client_spec.rb
201
+ - spec/atomic_cache/concerns/global_lmt_cache_concern_spec.rb
202
+ - spec/atomic_cache/default_config_spec.rb
203
+ - spec/atomic_cache/key/keyspace_spec.rb
204
+ - spec/atomic_cache/key/last_mod_time_key_manager_spec.rb
205
+ - spec/atomic_cache/storage/dalli_spec.rb
206
+ - spec/atomic_cache/storage/instance_memory_spec.rb
207
+ - spec/atomic_cache/storage/memory_spec.rb
208
+ - spec/atomic_cache/storage/shared_memory_spec.rb
209
+ - spec/spec_helper.rb
161
210
  homepage: https://github.com/ibotta/atomic_cache
162
211
  licenses:
163
- - apache 2.0
212
+ - Apache-2.0
164
213
  metadata: {}
165
214
  post_install_message:
166
215
  rdoc_options: []
@@ -178,7 +227,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
178
227
  version: 1.3.1
179
228
  requirements: []
180
229
  rubyforge_project:
181
- rubygems_version: 2.6.11
230
+ rubygems_version: 2.7.6
182
231
  signing_key:
183
232
  specification_version: 4
184
233
  summary: summary
data/.gitignore DELETED
@@ -1,51 +0,0 @@
1
- *.gem
2
- *.rbc
3
- .rspec_status
4
- /.config
5
- /coverage/
6
- /InstalledFiles
7
- /pkg/
8
- /spec/reports/
9
- /spec/examples.txt
10
- /test/tmp/
11
- /test/version_tmp/
12
- /tmp/
13
-
14
- # Used by dotenv library to load environment variables.
15
- # .env
16
-
17
- ## Specific to RubyMotion:
18
- .dat*
19
- .repl_history
20
- build/
21
- *.bridgesupport
22
- build-iPhoneOS/
23
- build-iPhoneSimulator/
24
-
25
- ## Specific to RubyMotion (use of CocoaPods):
26
- #
27
- # We recommend against adding the Pods directory to your .gitignore. However
28
- # you should judge for yourself, the pros and cons are mentioned at:
29
- # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
30
- #
31
- # vendor/Pods/
32
-
33
- ## Documentation cache and generated files:
34
- /.yardoc/
35
- /_yardoc/
36
- /doc/
37
- /rdoc/
38
-
39
- ## Environment normalization:
40
- /.bundle/
41
- /vendor/bundle
42
- /lib/bundler/man/
43
-
44
- # for a library or gem, you might want to ignore these files since the code is
45
- # intended to run in multiple environments; otherwise, check them in:
46
- Gemfile.lock
47
- # .ruby-version
48
- # .ruby-gemset
49
-
50
- # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
51
- .rvmrc
data/.ruby_version DELETED
@@ -1 +0,0 @@
1
- 2.4.3
data/.travis.yml DELETED
@@ -1,26 +0,0 @@
1
- language: ruby
2
-
3
- stages:
4
- - test-2.4
5
- - test-2.5
6
- - publish-gem
7
-
8
- jobs:
9
- include:
10
- - stage: test-2.4
11
- rvm: 2.4.3
12
- script:
13
- - bundle exec rake spec
14
-
15
- - stage: test-2.5
16
- rvm: 2.5.0
17
- before_script:
18
- - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
19
- - chmod +x ./cc-test-reporter
20
- - ./cc-test-reporter before-build
21
- script:
22
- - bundle exec rake spec
23
- after_script:
24
- - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
25
-
26
- - stage: publish-gem
data/CODE_OF_CONDUCT.md DELETED
@@ -1,46 +0,0 @@
1
- # Contributor Covenant Code of Conduct
2
-
3
- ## Our Pledge
4
-
5
- In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
6
-
7
- ## Our Standards
8
-
9
- Examples of behavior that contributes to creating a positive environment include:
10
-
11
- * Using welcoming and inclusive language
12
- * Being respectful of differing viewpoints and experiences
13
- * Gracefully accepting constructive criticism
14
- * Focusing on what is best for the community
15
- * Showing empathy towards other community members
16
-
17
- Examples of unacceptable behavior by participants include:
18
-
19
- * The use of sexualized language or imagery and unwelcome sexual attention or advances
20
- * Trolling, insulting/derogatory comments, and personal or political attacks
21
- * Public or private harassment
22
- * Publishing others' private information, such as a physical or electronic address, without explicit permission
23
- * Other conduct which could reasonably be considered inappropriate in a professional setting
24
-
25
- ## Our Responsibilities
26
-
27
- Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
28
-
29
- Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
30
-
31
- ## Scope
32
-
33
- This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
34
-
35
- ## Enforcement
36
-
37
- Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at osscompliance@ibotta.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
38
-
39
- Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
40
-
41
- ## Attribution
42
-
43
- This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
44
-
45
- [homepage]: http://contributor-covenant.org
46
- [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile DELETED
@@ -1,6 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- source 'https://rubygems.org'
4
-
5
- # Specify gem's dependencies in atomic_cache.gemspec
6
- gemspec
data/Rakefile DELETED
@@ -1,6 +0,0 @@
1
- require "bundler/gem_tasks"
2
- require "rspec/core/rake_task"
3
-
4
- RSpec::Core::RakeTask.new(:spec)
5
-
6
- task :default => :spec
data/atomic_cache.gemspec DELETED
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
- # coding: utf-8
3
- lib = File.expand_path('../lib', __FILE__)
4
- $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
5
- require 'atomic_cache/version'
6
-
7
- Gem::Specification.new do |spec|
8
- spec.name = 'atomic_cache'
9
- spec.version = AtomicCache::VERSION
10
- spec.authors = ['Ibotta Developers', 'Titus Stone']
11
- spec.email = 'osscompliance@ibotta.com'
12
-
13
- spec.summary = 'summary'
14
- spec.description = 'desc'
15
-
16
- spec.licenses = ['apache 2.0']
17
- spec.homepage = 'https://github.com/ibotta/atomic_cache'
18
-
19
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
- f.match(%r{^(test|spec|features)/})
21
- end
22
-
23
- spec.require_paths = ['lib']
24
-
25
- # Dev dependencies
26
- spec.add_development_dependency 'bundler', '~> 1.14'
27
- spec.add_development_dependency 'gems', '>= 1.0.0'
28
- spec.add_development_dependency 'rake', '~> 10.0'
29
- spec.add_development_dependency 'rspec', '~> 3.0'
30
- spec.add_development_dependency 'simplecov', '~> 0.15'
31
- spec.add_development_dependency 'timecop', '~> 0.8.1'
32
-
33
- # Dependencies
34
- spec.add_dependency 'activesupport', '>= 4.2'
35
- spec.add_dependency 'murmurhash3', '~> 0.1'
36
- end
data/bin/console DELETED
@@ -1,14 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require "bundler/setup"
4
- require "atomic_cache"
5
-
6
- # You can add fixtures and/or initialization code here to make experimenting
7
- # with your gem easier. You can also use a different console, if you like.
8
-
9
- # (If you use this, don't forget to add pry to your Gemfile!)
10
- # require "pry"
11
- # Pry.start
12
-
13
- require "irb"
14
- IRB.start(__FILE__)
data/bin/setup DELETED
@@ -1,8 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
- IFS=$'\n\t'
4
- set -vx
5
-
6
- bundle install
7
-
8
- # Do any other automated setup that you need to do here