atomic_cache 0.1.0.rc2 → 0.2.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|