atomic_cache 0.1.0.rc2 → 0.2.0.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 +4 -4
- data/README.md +1 -1
- data/docs/PROJECT_SETUP.md +8 -4
- data/docs/USAGE.md +2 -2
- data/lib/atomic_cache/atomic_cache_client.rb +1 -2
- data/lib/atomic_cache/concerns/global_lmt_cache_concern.rb +3 -3
- data/lib/atomic_cache/key/last_mod_time_key_manager.rb +1 -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 +1 -1
- data/spec/atomic_cache/concerns/global_lmt_cache_concern_spec.rb +19 -10
- data/spec/atomic_cache/key/last_mod_time_key_manager_spec.rb +6 -6
- data/spec/atomic_cache/storage/dalli_spec.rb +1 -1
- data/spec/atomic_cache/storage/memory_spec.rb +24 -8
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 4534d927a8015f910a8f6a330f5a49ffc2e96931b968bdafe123c104f50ee159
|
4
|
+
data.tar.gz: c8b834855c70ae59b272efd57eb9f491db734b90280a3d20f14ba4b56fe26786
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d1f49452ff1cf04f665112f39232749ec595bb6ac3a45f13ee178d52fe0e9581e594466f2936e542495f6dfda5b665617a77aebbd70e320b7a137d551fe1bf13
|
7
|
+
data.tar.gz: 113adcfe789cd195283c3bf2db1691c0724cf2b9e9fb5d1e3ac13bc93d401faac47b6206c54e8da045d666d27c913f617e931aa77e18b7f36c7a39b9cc2a41db
|
data/README.md
CHANGED
@@ -28,7 +28,7 @@ class Foo < ActiveRecord::Base
|
|
28
28
|
|
29
29
|
def active_foos(ids)
|
30
30
|
keyspace = cache_keyspace(:activeids, ids)
|
31
|
-
|
31
|
+
atomic_cache.fetch(keyspace, expires_in: 5.minutes) do
|
32
32
|
Foo.active.where(id: ids.uniq)
|
33
33
|
end
|
34
34
|
|
data/docs/PROJECT_SETUP.md
CHANGED
@@ -19,9 +19,13 @@ require 'datadog/statsd'
|
|
19
19
|
require 'atomic_cache'
|
20
20
|
|
21
21
|
AtomicCache::DefaultConfig.configure do |config|
|
22
|
-
config.logger
|
23
|
-
config.metrics
|
24
|
-
|
22
|
+
config.logger = Rails.logger
|
23
|
+
config.metrics = Datadog::Statsd.new('localhost', 8125, namespace: 'cache.atomic')
|
24
|
+
|
25
|
+
# note: these values can also be set in an env file for env-specific settings
|
26
|
+
config.namespace = 'atom'
|
27
|
+
config.cache_storage = AtomicCache::Storage::SharedMemory.new
|
28
|
+
config.key_storage = AtomicCache::Storage::SharedMemory.new
|
25
29
|
end
|
26
30
|
```
|
27
31
|
|
@@ -32,7 +36,7 @@ Note that `Datadog::Statsd` is not _required_. Adding it, however, will enable
|
|
32
36
|
* `key_storage` - Storage adapter for key manager (see below)
|
33
37
|
|
34
38
|
#### Optional
|
35
|
-
* `default_options` - Default options for every fetch call. See [options](
|
39
|
+
* `default_options` - Default options for every fetch call. See [fetch options](/Ibotta/atomic_cache/blob/master/docs/USAGE.md#fetch).
|
36
40
|
* `logger` - Logger instance. Used for debug and warn logs. Defaults to nil.
|
37
41
|
* `timestamp_formatter` - Proc to format last modified time for storage. Defaults to timestamp (`Time.to_i`)
|
38
42
|
* `metrics` - Metrics instance. Defaults to nil.
|
data/docs/USAGE.md
CHANGED
@@ -11,10 +11,10 @@ expire_cache(Time.now - 100) # an optional time can be given
|
|
11
11
|
The concern makes a `last_modified_time` method available both on the class and on the instance.
|
12
12
|
|
13
13
|
### Fetch
|
14
|
-
The concern makes a `
|
14
|
+
The concern makes a `atomic_cache` object available both on the class and on the instance.
|
15
15
|
|
16
16
|
```ruby
|
17
|
-
|
17
|
+
atomic_cache.fetch(options) do
|
18
18
|
# generate block
|
19
19
|
end
|
20
20
|
```
|
@@ -37,8 +37,7 @@ module AtomicCache
|
|
37
37
|
# @option options [Numeric] :max_retries (5) Max times to rety in waiting case
|
38
38
|
# @option options [Numeric] :backoff_duration_ms (50) Duration in ms to wait between retries
|
39
39
|
# @yield Generates a new value when cache is expired
|
40
|
-
def fetch(keyspace, options=
|
41
|
-
options ||= {}
|
40
|
+
def fetch(keyspace, options={})
|
42
41
|
key = @timestamp_manager.current_key(keyspace)
|
43
42
|
tags = ["cache_keyspace:#{keyspace.root}"]
|
44
43
|
|
@@ -15,7 +15,7 @@ module AtomicCache
|
|
15
15
|
|
16
16
|
class_methods do
|
17
17
|
|
18
|
-
def
|
18
|
+
def atomic_cache
|
19
19
|
init_atomic_cache
|
20
20
|
@atomic_cache
|
21
21
|
end
|
@@ -91,8 +91,8 @@ module AtomicCache
|
|
91
91
|
end
|
92
92
|
end
|
93
93
|
|
94
|
-
def
|
95
|
-
self.class.
|
94
|
+
def atomic_cache
|
95
|
+
self.class.atomic_cache
|
96
96
|
end
|
97
97
|
|
98
98
|
def cache_keyspace(ns)
|
@@ -54,7 +54,7 @@ module AtomicCache
|
|
54
54
|
# @param keyspace [AtomicCache::Keyspace] keyspace to lock
|
55
55
|
# @param ttl [Numeric] the duration in ms to lock (auto expires after duration is up)
|
56
56
|
# @param options [Hash] options to pass to the storage adapter
|
57
|
-
def lock(keyspace, ttl, options=
|
57
|
+
def lock(keyspace, ttl, options={})
|
58
58
|
@storage.add(keyspace.lock_key, LOCK_VALUE, ttl, options)
|
59
59
|
end
|
60
60
|
|
@@ -20,8 +20,8 @@ module AtomicCache
|
|
20
20
|
@dalli_client = dalli_client
|
21
21
|
end
|
22
22
|
|
23
|
-
def add(key, new_value, ttl, user_options=
|
24
|
-
opts = user_options
|
23
|
+
def add(key, new_value, ttl, user_options={})
|
24
|
+
opts = user_options.clone
|
25
25
|
opts[:raw] = true
|
26
26
|
|
27
27
|
# dalli expects time in seconds
|
@@ -31,13 +31,11 @@ module AtomicCache
|
|
31
31
|
response.start_with?(ADD_SUCCESS)
|
32
32
|
end
|
33
33
|
|
34
|
-
def read(key, user_options=
|
35
|
-
user_options ||= {}
|
34
|
+
def read(key, user_options={})
|
36
35
|
@dalli_client.read(key, user_options)
|
37
36
|
end
|
38
37
|
|
39
|
-
def set(key, value, user_options=
|
40
|
-
user_options ||= {}
|
38
|
+
def set(key, value, user_options={})
|
41
39
|
@dalli_client.set(key, value, user_options)
|
42
40
|
end
|
43
41
|
|
@@ -21,14 +21,13 @@ module AtomicCache
|
|
21
21
|
@store
|
22
22
|
end
|
23
23
|
|
24
|
-
def store_op(key, user_options=
|
24
|
+
def store_op(key, user_options={})
|
25
25
|
if !key.present?
|
26
26
|
desc = if key.nil? then 'Nil' else 'Empty' end
|
27
27
|
raise ArgumentError.new("#{desc} key given for storage operation") unless key.present?
|
28
28
|
end
|
29
29
|
|
30
30
|
normalized_key = key.to_sym
|
31
|
-
user_options ||= {}
|
32
31
|
yield(normalized_key, user_options)
|
33
32
|
end
|
34
33
|
|
@@ -12,35 +12,36 @@ module AtomicCache
|
|
12
12
|
def store; raise NotImplementedError end
|
13
13
|
|
14
14
|
# @abstract implement performing an operation on the store
|
15
|
-
def store_op(key, user_options=
|
15
|
+
def store_op(key, user_options={}); raise NotImplementedError end
|
16
16
|
|
17
|
-
def add(raw_key, new_value, ttl, user_options=
|
17
|
+
def add(raw_key, new_value, ttl, user_options={})
|
18
18
|
store_op(raw_key, user_options) do |key, options|
|
19
19
|
return false if store.has_key?(key)
|
20
|
-
write(key, new_value, ttl)
|
20
|
+
write(key, new_value, ttl, user_options)
|
21
21
|
end
|
22
22
|
end
|
23
23
|
|
24
|
-
def read(raw_key, user_options=
|
24
|
+
def read(raw_key, user_options={})
|
25
25
|
store_op(raw_key, user_options) do |key, options|
|
26
26
|
entry = store[key]
|
27
27
|
return nil unless entry.present?
|
28
28
|
|
29
|
-
|
29
|
+
unmarshaled = unmarshal(entry[:value], user_options)
|
30
|
+
return unmarshaled if entry[:ttl].nil? or entry[:ttl] == false
|
30
31
|
|
31
32
|
life = Time.now - entry[:written_at]
|
32
33
|
if (life >= entry[:ttl])
|
33
34
|
store.delete(key)
|
34
35
|
nil
|
35
36
|
else
|
36
|
-
|
37
|
+
unmarshaled
|
37
38
|
end
|
38
39
|
end
|
39
40
|
end
|
40
41
|
|
41
|
-
def set(raw_key, new_value, user_options=
|
42
|
+
def set(raw_key, new_value, user_options={})
|
42
43
|
store_op(raw_key, user_options) do |key, options|
|
43
|
-
write(key, new_value, options[:expires_in])
|
44
|
+
write(key, new_value, options[:expires_in], user_options)
|
44
45
|
end
|
45
46
|
end
|
46
47
|
|
@@ -51,12 +52,11 @@ module AtomicCache
|
|
51
52
|
end
|
52
53
|
end
|
53
54
|
|
54
|
-
|
55
|
-
stored_value = value.to_s
|
56
|
-
stored_value = nil if value.nil?
|
55
|
+
protected
|
57
56
|
|
57
|
+
def write(key, value, ttl=nil, user_options)
|
58
58
|
store[key] = {
|
59
|
-
value:
|
59
|
+
value: marshal(value, user_options),
|
60
60
|
ttl: ttl || false,
|
61
61
|
written_at: Time.now
|
62
62
|
}
|
@@ -26,6 +26,18 @@ module AtomicCache
|
|
26
26
|
# returns true if it succeeds; false otherwise
|
27
27
|
def delete(key, user_options); raise NotImplementedError end
|
28
28
|
|
29
|
+
protected
|
30
|
+
|
31
|
+
def marshal(value, user_options={})
|
32
|
+
return value if user_options[:raw]
|
33
|
+
Marshal.dump(value)
|
34
|
+
end
|
35
|
+
|
36
|
+
def unmarshal(value, user_options={})
|
37
|
+
return value if user_options[:raw]
|
38
|
+
Marshal.load(value)
|
39
|
+
end
|
40
|
+
|
29
41
|
end
|
30
42
|
end
|
31
43
|
end
|
data/lib/atomic_cache/version.rb
CHANGED
@@ -86,7 +86,7 @@ describe 'AtomicCacheClient' do
|
|
86
86
|
Timecop.freeze(time) do
|
87
87
|
subject.fetch(keyspace) { 'value from block' }
|
88
88
|
lmt = key_storage.read(timestamp_manager.last_modified_time_key)
|
89
|
-
expect(lmt).to eq(time.to_i
|
89
|
+
expect(lmt).to eq(time.to_i)
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
@@ -9,7 +9,12 @@ describe 'AtomicCacheConcern' do
|
|
9
9
|
subject do
|
10
10
|
class Foo1
|
11
11
|
include AtomicCache::GlobalLMTCacheConcern
|
12
|
+
|
13
|
+
def example_method
|
14
|
+
atomic_cache.fetch(cache_keyspace(:foo)) { 'bar' }
|
15
|
+
end
|
12
16
|
end
|
17
|
+
Foo1
|
13
18
|
end
|
14
19
|
|
15
20
|
before(:context) do
|
@@ -26,17 +31,21 @@ describe 'AtomicCacheConcern' do
|
|
26
31
|
cache_storage.reset
|
27
32
|
end
|
28
33
|
|
29
|
-
context '
|
34
|
+
context 'atomic_cache' do
|
30
35
|
it 'initializes a cache client' do
|
31
|
-
expect(subject).to respond_to(:
|
32
|
-
expect(subject.
|
33
|
-
expect(subject.new.
|
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)
|
34
39
|
end
|
35
40
|
|
36
41
|
it 'uses the name of the class in the default keyspace' do
|
37
42
|
subject.expire_cache
|
38
43
|
expect(key_storage.store).to have_key(:'foo1:lmt')
|
39
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
|
40
49
|
end
|
41
50
|
|
42
51
|
context '#expire_cache' do
|
@@ -44,7 +53,7 @@ describe 'AtomicCacheConcern' do
|
|
44
53
|
time = Time.local(2018, 1, 1, 15, 30, 0)
|
45
54
|
subject.expire_cache(time)
|
46
55
|
expect(key_storage.store).to have_key(:'foo1:lmt')
|
47
|
-
expect(key_storage.
|
56
|
+
expect(key_storage.read(:'foo1:lmt')).to eq(time.to_i)
|
48
57
|
end
|
49
58
|
|
50
59
|
it 'expires all the keyspaces for this class' do
|
@@ -54,8 +63,8 @@ describe 'AtomicCacheConcern' do
|
|
54
63
|
ns2 = subject.cache_keyspace(:buz)
|
55
64
|
|
56
65
|
Timecop.freeze(old_time) do
|
57
|
-
subject.
|
58
|
-
subject.
|
66
|
+
subject.atomic_cache.fetch(ns1) { 'bar' }
|
67
|
+
subject.atomic_cache.fetch(ns2) { 'buz' }
|
59
68
|
end
|
60
69
|
|
61
70
|
Timecop.freeze(new_time) do
|
@@ -66,8 +75,8 @@ describe 'AtomicCacheConcern' do
|
|
66
75
|
cache_storage.set("foo1:bar:#{lmt}", 'new-bar')
|
67
76
|
cache_storage.set("foo1:buz:#{lmt}", 'new-buz')
|
68
77
|
|
69
|
-
ns1_value = subject.
|
70
|
-
ns2_value = subject.
|
78
|
+
ns1_value = subject.atomic_cache.fetch(ns1)
|
79
|
+
ns2_value = subject.atomic_cache.fetch(ns2)
|
71
80
|
|
72
81
|
expect(ns1_value).to eq('new-bar')
|
73
82
|
expect(ns2_value).to eq('new-buz')
|
@@ -110,7 +119,7 @@ describe 'AtomicCacheConcern' do
|
|
110
119
|
end
|
111
120
|
|
112
121
|
it 'sets the storage for the class' do
|
113
|
-
cache_store = subject.
|
122
|
+
cache_store = subject.atomic_cache.instance_variable_get(:@storage)
|
114
123
|
expect(cache_store).to eq('valuestore')
|
115
124
|
|
116
125
|
key_store = subject.instance_variable_get(:@timestamp_manager).instance_variable_get(:@storage)
|
@@ -42,22 +42,22 @@ describe 'LastModTimeKeyManager' do
|
|
42
42
|
|
43
43
|
it 'promotes a timestamp and last known key' do
|
44
44
|
subject.promote(req_keyspace, last_known_key: 'asdf', timestamp: timestamp)
|
45
|
-
expect(storage.
|
46
|
-
expect(storage.
|
47
|
-
expect(subject.last_modified_time).to eq(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
48
|
end
|
49
49
|
|
50
50
|
context '#last_modified_time=' do
|
51
51
|
it 'returns the last modified time' do
|
52
52
|
subject.last_modified_time = timestamp
|
53
|
-
expect(storage.
|
54
|
-
expect(subject.last_modified_time).to eq(timestamp
|
53
|
+
expect(storage.read(:'ts:lmt')).to eq(timestamp)
|
54
|
+
expect(subject.last_modified_time).to eq(timestamp)
|
55
55
|
end
|
56
56
|
|
57
57
|
it 'formats Time' do
|
58
58
|
now = Time.now
|
59
59
|
subject.last_modified_time = now
|
60
|
-
expect(subject.last_modified_time).to eq(now.to_i
|
60
|
+
expect(subject.last_modified_time).to eq(now.to_i)
|
61
61
|
end
|
62
62
|
end
|
63
63
|
|
@@ -12,13 +12,13 @@ shared_examples 'memory storage' do
|
|
12
12
|
result = subject.add('key', 'value', 100)
|
13
13
|
|
14
14
|
expect(subject.store).to have_key(:key)
|
15
|
-
expect(subject.store[:key][:value]).to eq('value')
|
15
|
+
expect(Marshal.load(subject.store[:key][:value])).to eq('value')
|
16
16
|
expect(subject.store[:key][:ttl]).to eq(100)
|
17
17
|
expect(result).to eq(true)
|
18
18
|
end
|
19
19
|
|
20
20
|
it 'does not write the key if it exists' do
|
21
|
-
entry = { value: 'foo', ttl: 100, written_at: 100 }
|
21
|
+
entry = { value: Marshal.dump('foo'), ttl: 100, written_at: 100 }
|
22
22
|
subject.store[:key] = entry
|
23
23
|
|
24
24
|
result = subject.add('key', 'value', 200)
|
@@ -26,28 +26,44 @@ shared_examples 'memory storage' do
|
|
26
26
|
|
27
27
|
# stored values should not have changed
|
28
28
|
expect(subject.store).to have_key(:key)
|
29
|
-
expect(subject.store[:key][:value]).to eq('foo')
|
29
|
+
expect(Marshal.load(subject.store[:key][:value])).to eq('foo')
|
30
30
|
expect(subject.store[:key][:ttl]).to eq(100)
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
34
|
context '#read' do
|
35
35
|
it 'returns values' do
|
36
|
-
subject.store[:sugar] = { value: 'foo' }
|
36
|
+
subject.store[:sugar] = { value: Marshal.dump('foo') }
|
37
37
|
expect(subject.read('sugar')).to eq('foo')
|
38
38
|
end
|
39
39
|
|
40
40
|
it 'respects TTL' do
|
41
|
-
subject.store[:sugar] = { value: 'foo', ttl: 100, written_at: Time.now - 1000 }
|
41
|
+
subject.store[:sugar] = { value: Marshal.dump('foo'), ttl: 100, written_at: Time.now - 1000 }
|
42
42
|
expect(subject.read('sugar')).to eq(nil)
|
43
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
|
44
60
|
end
|
45
61
|
|
46
62
|
context '#set' do
|
47
63
|
it 'adds the value when not present' do
|
48
64
|
subject.set(:cane, 'v', expires_in: 100)
|
49
65
|
expect(subject.store).to have_key(:cane)
|
50
|
-
expect(subject.store[:cane][:value]).to eq('v')
|
66
|
+
expect(Marshal.load(subject.store[:cane][:value])).to eq('v')
|
51
67
|
expect(subject.store[:cane][:ttl]).to eq(100)
|
52
68
|
end
|
53
69
|
|
@@ -56,14 +72,14 @@ shared_examples 'memory storage' do
|
|
56
72
|
|
57
73
|
subject.set(:cane, 'v', expires_in: 100)
|
58
74
|
expect(subject.store).to have_key(:cane)
|
59
|
-
expect(subject.store[:cane][:value]).to eq('v')
|
75
|
+
expect(Marshal.load(subject.store[:cane][:value])).to eq('v')
|
60
76
|
expect(subject.store[:cane][:ttl]).to eq(100)
|
61
77
|
end
|
62
78
|
end
|
63
79
|
|
64
80
|
context '#delete' do
|
65
81
|
it 'deletes the key' do
|
66
|
-
subject.store[:record] = { value: 'foo', written_at: 500 }
|
82
|
+
subject.store[:record] = { value: Marshal.dump('foo'), written_at: 500 }
|
67
83
|
subject.delete('record')
|
68
84
|
expect(subject.store).to_not have_key(:record)
|
69
85
|
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.
|
4
|
+
version: 0.2.0.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: 2018-02-
|
12
|
+
date: 2018-02-28 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|