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.
@@ -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,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,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