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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 86d21873e0440ffb327b3d9e6de724ffff524d7075c5f4588e4a4c46042cf47a
4
- data.tar.gz: 956391de071f7997ccd078157449634436e3ed6316215bf6319e830c979fe0d0
3
+ metadata.gz: 4534d927a8015f910a8f6a330f5a49ffc2e96931b968bdafe123c104f50ee159
4
+ data.tar.gz: c8b834855c70ae59b272efd57eb9f491db734b90280a3d20f14ba4b56fe26786
5
5
  SHA512:
6
- metadata.gz: 72eca9fdf5c4018f8934917219abb4ccde4cba7fae17482550b2087673eacec18cae5e5cb6686a00fb317424bae379ddb83cfc50f767ae456b936ac71fe8a702
7
- data.tar.gz: 4739607a81c343ba2b73de9f7e36d18d4c1a76969f51d7ab4c3cbbdf54fb3274e6bc9d0244a75e8b4a8fada4aea59aa0ce5b05ed397085a5ef606fe801afd233
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
- AtomicCache.fetch(keyspace, expires_in: 5.minutes) do
31
+ atomic_cache.fetch(keyspace, expires_in: 5.minutes) do
32
32
  Foo.active.where(id: ids.uniq)
33
33
  end
34
34
 
@@ -19,9 +19,13 @@ require 'datadog/statsd'
19
19
  require 'atomic_cache'
20
20
 
21
21
  AtomicCache::DefaultConfig.configure do |config|
22
- config.logger = Rails.logger
23
- config.metrics = Datadog::Statsd.new('localhost', 8125, namespace: 'cache.atomic')
24
- config.namespace = 'atom'
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](TODO: LINK).
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 `AtomicCache` object available both on the class and on the instance.
14
+ The concern makes a `atomic_cache` object available both on the class and on the instance.
15
15
 
16
16
  ```ruby
17
- AtomicCache.fetch(options) do
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=nil)
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 AtomicCache
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 AtomicCache
95
- self.class.AtomicCache
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=nil)
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=nil)
24
- opts = user_options&.clone || {}
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=nil)
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=nil)
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=nil)
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=nil); raise NotImplementedError end
15
+ def store_op(key, user_options={}); raise NotImplementedError end
16
16
 
17
- def add(raw_key, new_value, ttl, user_options=nil)
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=nil)
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
- return entry[:value] if entry[:ttl].nil? or entry[:ttl] == false
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
- entry[:value]
37
+ unmarshaled
37
38
  end
38
39
  end
39
40
  end
40
41
 
41
- def set(raw_key, new_value, user_options=nil)
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
- def write(key, value, ttl=nil)
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: stored_value,
59
+ value: marshal(value, user_options),
60
60
  ttl: ttl || false,
61
61
  written_at: Time.now
62
62
  }
@@ -26,10 +26,8 @@ module AtomicCache
26
26
  STORE
27
27
  end
28
28
 
29
- def store_op(key, user_options=nil)
29
+ def store_op(key, user_options={})
30
30
  normalized_key = key.to_sym
31
- user_options ||= {}
32
-
33
31
  SEMAPHORE.synchronize do
34
32
  yield(normalized_key, user_options)
35
33
  end
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AtomicCache
4
- VERSION = "0.1.0.rc2"
4
+ VERSION = "0.2.0.rc1"
5
5
  end
@@ -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.to_s)
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 'AtomicCache' do
34
+ context 'atomic_cache' do
30
35
  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)
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.store[:'foo1:lmt'][:value]).to eq(time.to_i.to_s)
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.AtomicCache.fetch(ns1) { 'bar' }
58
- subject.AtomicCache.fetch(ns2) { 'buz' }
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.AtomicCache.fetch(ns1)
70
- ns2_value = subject.AtomicCache.fetch(ns2)
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.AtomicCache.instance_variable_get(:@storage)
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.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)
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.store[:'ts:lmt'][:value]).to eq(timestamp.to_s)
54
- expect(subject.last_modified_time).to eq(timestamp.to_s)
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.to_s)
60
+ expect(subject.last_modified_time).to eq(now.to_i)
61
61
  end
62
62
  end
63
63
 
@@ -19,7 +19,7 @@ describe 'Dalli' do
19
19
  end
20
20
 
21
21
  it 'delegates #read without options' do
22
- expect(dalli_client).to receive(:read).with('key', {})
22
+ expect(dalli_client).to receive(:read).with('key', {}).and_return('asdf')
23
23
  subject.read('key')
24
24
  end
25
25
 
@@ -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.1.0.rc2
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-23 00:00:00.000000000 Z
12
+ date: 2018-02-28 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler