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 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