atomic_cache 0.1.0.rc1 → 0.2.1.rc2
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 +5 -5
- data/README.md +12 -4
- data/docs/PROJECT_SETUP.md +10 -7
- data/docs/USAGE.md +3 -3
- data/lib/atomic_cache/atomic_cache_client.rb +1 -2
- data/lib/atomic_cache/concerns/global_lmt_cache_concern.rb +4 -4
- data/lib/atomic_cache/key/keyspace.rb +4 -4
- data/lib/atomic_cache/key/last_mod_time_key_manager.rb +2 -1
- data/lib/atomic_cache/storage/dalli.rb +4 -6
- data/lib/atomic_cache/storage/instance_memory.rb +1 -2
- data/lib/atomic_cache/storage/memory.rb +12 -12
- data/lib/atomic_cache/storage/shared_memory.rb +1 -3
- data/lib/atomic_cache/storage/store.rb +12 -0
- data/lib/atomic_cache/version.rb +1 -1
- data/spec/atomic_cache/atomic_cache_client_spec.rb +213 -0
- data/spec/atomic_cache/concerns/global_lmt_cache_concern_spec.rb +137 -0
- data/spec/atomic_cache/default_config_spec.rb +22 -0
- data/spec/atomic_cache/key/keyspace_spec.rb +64 -0
- data/spec/atomic_cache/key/last_mod_time_key_manager_spec.rb +64 -0
- data/spec/atomic_cache/storage/dalli_spec.rb +61 -0
- data/spec/atomic_cache/storage/instance_memory_spec.rb +9 -0
- data/spec/atomic_cache/storage/memory_spec.rb +88 -0
- data/spec/atomic_cache/storage/shared_memory_spec.rb +9 -0
- data/spec/spec_helper.rb +20 -0
- metadata +64 -16
- data/.gitignore +0 -51
- data/.ruby_version +0 -1
- data/.travis.yml +0 -26
- data/CODE_OF_CONDUCT.md +0 -46
- data/Gemfile +0 -6
- data/Rakefile +0 -6
- data/atomic_cache.gemspec +0 -36
- data/bin/console +0 -14
- data/bin/setup +0 -8
@@ -0,0 +1,137 @@
|
|
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
|
+
|
13
|
+
def example_method
|
14
|
+
atomic_cache.fetch(cache_keyspace(:foo)) { 'bar' }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
Foo1
|
18
|
+
end
|
19
|
+
|
20
|
+
before(:context) do
|
21
|
+
DefaultConfig.instance.reset
|
22
|
+
DefaultConfig.configure do |cfg|
|
23
|
+
cfg.cache_storage = AtomicCache::Storage::SharedMemory.new
|
24
|
+
cfg.key_storage = AtomicCache::Storage::SharedMemory.new
|
25
|
+
cfg.timestamp_formatter = Proc.new { |time| time.to_i }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
before(:each) do
|
30
|
+
key_storage.reset
|
31
|
+
cache_storage.reset
|
32
|
+
end
|
33
|
+
|
34
|
+
context 'atomic_cache' do
|
35
|
+
it 'initializes a cache client' do
|
36
|
+
expect(subject).to respond_to(:atomic_cache)
|
37
|
+
expect(subject.atomic_cache).to be_a(AtomicCacheClient)
|
38
|
+
expect(subject.new.atomic_cache).to be_a(AtomicCacheClient)
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'uses the name of the class in the default keyspace' do
|
42
|
+
subject.expire_cache
|
43
|
+
expect(key_storage.store).to have_key(:'foo1:lmt')
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'allows methods to be defined that utilize the cache' do
|
47
|
+
expect(subject.new.example_method).to eq('bar')
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context '#expire_cache' do
|
52
|
+
it 'updates the last modified time' do
|
53
|
+
time = Time.local(2018, 1, 1, 15, 30, 0)
|
54
|
+
subject.expire_cache(time)
|
55
|
+
expect(key_storage.store).to have_key(:'foo1:lmt')
|
56
|
+
expect(key_storage.read(:'foo1:lmt')).to eq(time.to_i)
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'expires all the keyspaces for this class' do
|
60
|
+
old_time = Time.local(2018, 1, 1, 15, 30, 0)
|
61
|
+
new_time = Time.local(2018, 1, 1, 15, 40, 0)
|
62
|
+
ns1 = subject.cache_keyspace(:bar)
|
63
|
+
ns2 = subject.cache_keyspace(:buz)
|
64
|
+
|
65
|
+
Timecop.freeze(old_time) do
|
66
|
+
subject.atomic_cache.fetch(ns1) { 'bar' }
|
67
|
+
subject.atomic_cache.fetch(ns2) { 'buz' }
|
68
|
+
end
|
69
|
+
|
70
|
+
Timecop.freeze(new_time) do
|
71
|
+
subject.expire_cache
|
72
|
+
lmt = subject.last_modified_time
|
73
|
+
|
74
|
+
# some other process writes new values
|
75
|
+
cache_storage.set("foo1:bar:#{lmt}", 'new-bar')
|
76
|
+
cache_storage.set("foo1:buz:#{lmt}", 'new-buz')
|
77
|
+
|
78
|
+
ns1_value = subject.atomic_cache.fetch(ns1)
|
79
|
+
ns2_value = subject.atomic_cache.fetch(ns2)
|
80
|
+
|
81
|
+
expect(ns1_value).to eq('new-bar')
|
82
|
+
expect(ns2_value).to eq('new-buz')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'works on the instance method' do
|
87
|
+
time = Time.local(2018, 1, 1, 15, 30, 0)
|
88
|
+
subject.new.expire_cache(time)
|
89
|
+
expect(key_storage.store).to have_key(:'foo1:lmt')
|
90
|
+
expect(key_storage.read(:'foo1:lmt')).to eq(time.to_i)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
context '#cache_keyspace' do
|
95
|
+
it 'returns a child keyspace of the class keyspace' do
|
96
|
+
ns = subject.cache_keyspace(:fuz, :baz)
|
97
|
+
expect(ns).to be_a(Keyspace)
|
98
|
+
expect(ns.namespace).to eq(['foo1', :fuz, :baz])
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'keyspace macros' do
|
103
|
+
subject do
|
104
|
+
class Foo2
|
105
|
+
include AtomicCache::GlobalLMTCacheConcern
|
106
|
+
cache_version(3)
|
107
|
+
cache_class('foo')
|
108
|
+
end
|
109
|
+
Foo2
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'uses the given version and cache_class become part of the cache keyspace' do
|
113
|
+
subject.expire_cache
|
114
|
+
expect(key_storage.store).to have_key(:'foo:v3:lmt')
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
context 'storage macros' do
|
119
|
+
subject do
|
120
|
+
class Foo3
|
121
|
+
include AtomicCache::GlobalLMTCacheConcern
|
122
|
+
cache_key_storage('keystore')
|
123
|
+
cache_value_storage('valuestore')
|
124
|
+
end
|
125
|
+
Foo3
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'sets the storage for the class' do
|
129
|
+
cache_store = subject.atomic_cache.instance_variable_get(:@storage)
|
130
|
+
expect(cache_store).to eq('valuestore')
|
131
|
+
|
132
|
+
key_store = subject.instance_variable_get(:@timestamp_manager).instance_variable_get(:@storage)
|
133
|
+
expect(key_store).to eq('keystore')
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
@@ -0,0 +1,22 @@
|
|
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
|
+
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')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,64 @@
|
|
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
|
+
|
10
|
+
it 'sorts sortable values before hashing' do
|
11
|
+
ks1 = AtomicCache::Keyspace.new(namespace: ['foo', [1, 2, 3]])
|
12
|
+
ks2 = AtomicCache::Keyspace.new(namespace: ['foo', [3, 2, 1]])
|
13
|
+
expect(ks1.namespace).to eq(ks2.namespace)
|
14
|
+
end
|
15
|
+
|
16
|
+
context 'namespace' do
|
17
|
+
it 'accepts nil' do
|
18
|
+
ks = AtomicCache::Keyspace.new(namespace: nil)
|
19
|
+
expect(ks.namespace).to eq([])
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'accepts single values' do
|
23
|
+
ks = AtomicCache::Keyspace.new(namespace: 'foo')
|
24
|
+
expect(ks.namespace).to eq(['foo'])
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'hashes non-primitive types' do
|
28
|
+
ids = [1,2,3]
|
29
|
+
ks1 = AtomicCache::Keyspace.new(namespace: ['foo', ids])
|
30
|
+
hash = ks1.send(:hexhash, ids)
|
31
|
+
expect(ks1.namespace).to eq(['foo', hash])
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'leaves primitives alone' do
|
35
|
+
ks1 = AtomicCache::Keyspace.new(namespace: ['foo', :foo, 5])
|
36
|
+
expect(ks1.namespace).to eq(['foo', :foo, 5])
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'expands timestamps' do
|
40
|
+
formatter = Proc.new { |t| 'formatted' }
|
41
|
+
ks = AtomicCache::Keyspace.new(namespace: Time.new, timestamp_formatter: formatter)
|
42
|
+
expect(ks.namespace).to eq(['formatted'])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
context '#child' do
|
48
|
+
it 'extends the keyspace' do
|
49
|
+
ks2 = subject.child([:buz, :baz])
|
50
|
+
expect(ks2.namespace).to eq(['foo', 'bar', :buz, :baz])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
context '#key' do
|
55
|
+
it 'return a key of the segments' do
|
56
|
+
expect(subject.key).to eq('foo:bar')
|
57
|
+
end
|
58
|
+
|
59
|
+
it 'return the key with the suffix' do
|
60
|
+
expect(subject.key('baz')).to eq('foo:bar:baz')
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
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.read(:'ns:lkk')).to eq('asdf')
|
46
|
+
expect(storage.read(:'ts:lmt')).to eq(timestamp)
|
47
|
+
expect(subject.last_modified_time).to eq(timestamp)
|
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.read(:'ts:lmt')).to eq(timestamp)
|
54
|
+
expect(subject.last_modified_time).to eq(timestamp)
|
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)
|
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', {}).and_return('asdf')
|
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,88 @@
|
|
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(Marshal.load(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: Marshal.dump('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(Marshal.load(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: Marshal.dump('foo') }
|
37
|
+
expect(subject.read('sugar')).to eq('foo')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'respects TTL' do
|
41
|
+
subject.store[:sugar] = { value: Marshal.dump('foo'), ttl: 100, written_at: Time.now - 1000 }
|
42
|
+
expect(subject.read('sugar')).to eq(nil)
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'returns complex objects' do
|
46
|
+
class ComplexObject
|
47
|
+
attr_accessor :foo, :bar
|
48
|
+
end
|
49
|
+
|
50
|
+
obj = ComplexObject.new
|
51
|
+
obj.foo = 'f'
|
52
|
+
obj.bar = [1,2,3]
|
53
|
+
|
54
|
+
subject.set(:complex, obj)
|
55
|
+
|
56
|
+
obj2 = subject.read(:complex)
|
57
|
+
expect(obj2.foo).to eql('f')
|
58
|
+
expect(obj2.bar).to eql([1,2,3])
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context '#set' do
|
63
|
+
it 'adds the value when not present' do
|
64
|
+
subject.set(:cane, 'v', expires_in: 100)
|
65
|
+
expect(subject.store).to have_key(:cane)
|
66
|
+
expect(Marshal.load(subject.store[:cane][:value])).to eq('v')
|
67
|
+
expect(subject.store[:cane][:ttl]).to eq(100)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'overwrites existing values' do
|
71
|
+
subject.store[:cane] = { value: 'foo', ttl: 500, written_at: 500 }
|
72
|
+
|
73
|
+
subject.set(:cane, 'v', expires_in: 100)
|
74
|
+
expect(subject.store).to have_key(:cane)
|
75
|
+
expect(Marshal.load(subject.store[:cane][:value])).to eq('v')
|
76
|
+
expect(subject.store[:cane][:ttl]).to eq(100)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context '#delete' do
|
81
|
+
it 'deletes the key' do
|
82
|
+
subject.store[:record] = { value: Marshal.dump('foo'), written_at: 500 }
|
83
|
+
subject.delete('record')
|
84
|
+
expect(subject.store).to_not have_key(:record)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|