atomic_cache 0.5.0.rc1 → 0.5.4.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/docs/MODEL_SETUP.md +62 -0
- data/docs/USAGE.md +6 -0
- data/lib/atomic_cache/atomic_cache_client.rb +4 -6
- data/lib/atomic_cache/concerns/global_lmt_cache_concern.rb +16 -2
- data/lib/atomic_cache/key/last_mod_time_key_manager.rb +8 -2
- data/lib/atomic_cache/version.rb +1 -1
- data/spec/atomic_cache/atomic_cache_client_spec.rb +0 -7
- data/spec/atomic_cache/concerns/global_lmt_cache_concern_spec.rb +55 -0
- data/spec/atomic_cache/key/last_mod_time_key_manager_spec.rb +9 -0
- metadata +20 -12
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e912c124c9879b3208c849a21f6f82a665ddc0f3ab1e268a1355d6cfb7bda54a
|
4
|
+
data.tar.gz: 3fc8b8814852f95414e8c699fb2b6f17d4c0a0516f7bc12dbdb2d167096604ac
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5104f60b2f4f9d2816a53e7a19dcf58eda5e245b052c1d048c586cae8df985e0eb17f58c13bdc8dc07db4bb50607d033cb57c7a701db50a896955575cc710e3b
|
7
|
+
data.tar.gz: bae2d03aac48378a6075d648c9131cf265271192658e30b5191a6bb82735e7a4aae0162d62b0c4153ed46ad16d9375cf8c832bd899b408e3ab7a9617710d2f71
|
data/docs/MODEL_SETUP.md
CHANGED
@@ -29,3 +29,65 @@ class Foo < ActiveRecord::Base
|
|
29
29
|
cache_version(5)
|
30
30
|
end
|
31
31
|
```
|
32
|
+
|
33
|
+
### Inheritance
|
34
|
+
|
35
|
+
When either `force_cache_class` or `cache_version` are used, those values will always be preferred by classes which inherit from the class on which those macros exist. When macros are used on descendant classes, the "closet" value wins.
|
36
|
+
|
37
|
+
#### Example #1
|
38
|
+
The keys used by `Bar` will be prefixed with `custom:v5`, as it prefers both the 'custom' class name and version 5 from the parent.
|
39
|
+
```ruby
|
40
|
+
class Foo < ActiveRecord::Base
|
41
|
+
include AtomicCache::GlobalLMTCacheConcern
|
42
|
+
force_cache_class('custom')
|
43
|
+
cache_version(5)
|
44
|
+
end
|
45
|
+
|
46
|
+
class Bar < Foo
|
47
|
+
end
|
48
|
+
```
|
49
|
+
|
50
|
+
#### Example #2
|
51
|
+
The keys used by `Bar` will be prefixed with `bar:v5`, as the version 5 is taken from the parent, but the use of forcing on the child class result in the cache class of 'bar'.
|
52
|
+
```ruby
|
53
|
+
class Foo < ActiveRecord::Base
|
54
|
+
include AtomicCache::GlobalLMTCacheConcern
|
55
|
+
cache_version(5)
|
56
|
+
end
|
57
|
+
|
58
|
+
class Bar < Foo
|
59
|
+
force_cache_class('bar')
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
#### Example #3
|
64
|
+
The keys used by `Bar` will be still be prefixed with `bar:v5` for the same reasons as above.
|
65
|
+
```ruby
|
66
|
+
class Foo < ActiveRecord::Base
|
67
|
+
include AtomicCache::GlobalLMTCacheConcern
|
68
|
+
force_cache_class('custom')
|
69
|
+
cache_version(5)
|
70
|
+
end
|
71
|
+
|
72
|
+
class Bar < Foo
|
73
|
+
force_cache_class('bar')
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
### Rails Model Inheritance
|
78
|
+
|
79
|
+
It's not uncommon in rails to end up with model inhertiance. For example:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
class Content < ActiveRecord::Base
|
83
|
+
include AtomicCache::GlobalLMTCacheConcern
|
84
|
+
force_cache_class('content')
|
85
|
+
end
|
86
|
+
|
87
|
+
class BlogPost < Content
|
88
|
+
end
|
89
|
+
```
|
90
|
+
|
91
|
+
If these models will be cached together into a single key, it's preferable to force the cache class on the parent, causing all the descendant types to use the same keyspace. Not doing this will cause each subtype to use it's own last modified time.
|
92
|
+
|
93
|
+
If the models will be treated as separate collections and cached separately, this is not recommended. Alternately, if only some subtypes will be cached together, those should share a forced cache class and version.
|
data/docs/USAGE.md
CHANGED
@@ -38,6 +38,12 @@ The ideal `generate_ttl_ms` time is just slightly longer than the average genera
|
|
38
38
|
|
39
39
|
If metrics are enabled, the `<namespace>.generate.run` can be used to determine the min/max/average generate time for a particular cache and the `generate_ttl_ms` tuned using that.
|
40
40
|
|
41
|
+
##### ⚠️ TTL Rounding
|
42
|
+
When using atomic_cache with memcached, be aware that the TTL will be rounded down to the nearest whole seconds. For example, a `generate_ttl_ms` value of 3500 will result in a 3s TTL with memcache.
|
43
|
+
|
44
|
+
##### ⚠️ Max Rate of Change
|
45
|
+
Atomic_cache will *not* remove the lock after the generation process is done. This is both more efficient, and allows the `generate_ttl_ms` to be used to limit the total rate of change. For example, if the typical database query behind the cache takes 2s to run, but `generate_ttl_ms` is set to 10s, then the lock will live on for 8s after the generate process finishes, preventing other processes from querying for a new value. In many ways, `generate_ttl_ms` is the amount of time that the system will be un-allowed to make additional queries.
|
46
|
+
|
41
47
|
#### `max_retries` & `backoff_duration_ms`
|
42
48
|
_`max_retries` defaults to 5._
|
43
49
|
_`backoff_duration_ms` defaults to 50ms._
|
@@ -93,8 +93,8 @@ module AtomicCache
|
|
93
93
|
end
|
94
94
|
|
95
95
|
new_key = @timestamp_manager.next_key(keyspace, lmt)
|
96
|
-
@timestamp_manager.promote(keyspace, last_known_key: new_key, timestamp: lmt)
|
97
96
|
@storage.set(new_key, new_value, options)
|
97
|
+
@timestamp_manager.promote(keyspace, last_known_key: new_key, timestamp: lmt)
|
98
98
|
|
99
99
|
metrics(:increment, 'generate.current-thread', tags: tags)
|
100
100
|
log(:debug, "Generating new value for `#{new_key}`")
|
@@ -119,13 +119,11 @@ module AtomicCache
|
|
119
119
|
return lkv
|
120
120
|
end
|
121
121
|
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
@storage.delete(lkk)
|
122
|
+
metrics(:increment, 'last-known-value.nil', tags: tags)
|
123
|
+
else
|
124
|
+
metrics(:increment, 'last-known-value.not-present', tags: tags)
|
126
125
|
end
|
127
126
|
|
128
|
-
metrics(:increment, 'last-known-value.not-present', tags: tags)
|
129
127
|
nil
|
130
128
|
end
|
131
129
|
|
@@ -63,15 +63,29 @@ module AtomicCache
|
|
63
63
|
@timestamp_manager.last_modified_time
|
64
64
|
end
|
65
65
|
|
66
|
+
# protected
|
67
|
+
|
68
|
+
def cache_version_get
|
69
|
+
return @atomic_cache_version if @atomic_cache_version.present?
|
70
|
+
return self.superclass.cache_version_get if self.superclass.respond_to?(:cache_version_get)
|
71
|
+
nil
|
72
|
+
end
|
73
|
+
|
74
|
+
def cache_class_get
|
75
|
+
return @atomic_cache_class if @atomic_cache_class.present?
|
76
|
+
return self.superclass.cache_class_get if self.superclass.respond_to?(:cache_class_get)
|
77
|
+
nil
|
78
|
+
end
|
79
|
+
|
66
80
|
private
|
67
81
|
|
68
82
|
def init_atomic_cache
|
69
83
|
return if @atomic_cache.present?
|
70
84
|
ATOMIC_CACHE_CONCERN_MUTEX.synchronize do
|
71
|
-
cache_class =
|
85
|
+
cache_class = cache_class_get || default_cache_class
|
72
86
|
prefix = [cache_class]
|
73
87
|
prefix.unshift(DefaultConfig.instance.namespace) if DefaultConfig.instance.namespace.present?
|
74
|
-
prefix.push("v#{
|
88
|
+
prefix.push("v#{cache_version_get}") if cache_version_get.present?
|
75
89
|
@default_cache_keyspace = Keyspace.new(namespace: prefix, root: cache_class)
|
76
90
|
|
77
91
|
@timestamp_manager = LastModTimeKeyManager.new(
|
@@ -44,8 +44,7 @@ module AtomicCache
|
|
44
44
|
# @param last_known_key [String] a key with a known value to refer other processes to
|
45
45
|
# @param timestamp [String, Numeric, Time] the timestamp with which the last_known_key was updated at
|
46
46
|
def promote(keyspace, last_known_key:, timestamp:)
|
47
|
-
|
48
|
-
@storage.set(key, last_known_key)
|
47
|
+
@storage.set(keyspace.last_known_key_key, last_known_key)
|
49
48
|
@storage.set(last_modified_time_key, self.format(timestamp))
|
50
49
|
end
|
51
50
|
|
@@ -59,6 +58,13 @@ module AtomicCache
|
|
59
58
|
@storage.add(keyspace.lock_key, LOCK_VALUE, ttl, options)
|
60
59
|
end
|
61
60
|
|
61
|
+
# check if the keyspace is locked
|
62
|
+
#
|
63
|
+
# @param keyspace [AtomicCache::Keyspace] keyspace to lock
|
64
|
+
def lock_present?(keyspace)
|
65
|
+
@storage.read(keyspace.lock_key) == LOCK_VALUE
|
66
|
+
end
|
67
|
+
|
62
68
|
# remove existing lock to allow other processes to update keyspace
|
63
69
|
#
|
64
70
|
# @param keyspace [AtomicCache::Keyspace] keyspace to lock
|
data/lib/atomic_cache/version.rb
CHANGED
@@ -168,13 +168,6 @@ describe 'AtomicCacheClient' do
|
|
168
168
|
result = subject.fetch(keyspace, backoff_duration_ms: 5) { 'value from generate' }
|
169
169
|
expect(result).to eq(nil)
|
170
170
|
end
|
171
|
-
|
172
|
-
it 'deletes the last known key' do
|
173
|
-
key_storage.set(keyspace.last_known_key_key, :oldkey)
|
174
|
-
cache_storage.set(:oldkey, nil)
|
175
|
-
subject.fetch(keyspace, backoff_duration_ms: 5) { 'value from generate' }
|
176
|
-
expect(cache_storage.store).to_not have_key(:oldkey)
|
177
|
-
end
|
178
171
|
end
|
179
172
|
end
|
180
173
|
end
|
@@ -113,6 +113,61 @@ describe 'AtomicCacheConcern' do
|
|
113
113
|
subject.expire_cache
|
114
114
|
expect(key_storage.store).to have_key(:'foo:v3:lmt')
|
115
115
|
end
|
116
|
+
|
117
|
+
context 'with version < class inheritance' do
|
118
|
+
subject do
|
119
|
+
class Foo3
|
120
|
+
include AtomicCache::GlobalLMTCacheConcern
|
121
|
+
cache_version(6)
|
122
|
+
end
|
123
|
+
class Foo4 < Foo3
|
124
|
+
force_cache_class('foo')
|
125
|
+
end
|
126
|
+
Foo4
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'uses the version from the parent and the forced class name from the macro' do
|
130
|
+
subject.expire_cache
|
131
|
+
expect(key_storage.store).to have_key(:'foo:v6:lmt')
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
context 'with class < version inheritance' do
|
136
|
+
subject do
|
137
|
+
class Foo5
|
138
|
+
include AtomicCache::GlobalLMTCacheConcern
|
139
|
+
force_cache_class('foo')
|
140
|
+
end
|
141
|
+
class Foo6 < Foo5
|
142
|
+
cache_version(6)
|
143
|
+
end
|
144
|
+
Foo6
|
145
|
+
end
|
146
|
+
|
147
|
+
it 'uses the cache class from the parent and version from the macro' do
|
148
|
+
subject.expire_cache
|
149
|
+
expect(key_storage.store).to have_key(:'foo:v6:lmt')
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
context 'dual force_cache_class' do
|
154
|
+
subject do
|
155
|
+
class Foo6
|
156
|
+
include AtomicCache::GlobalLMTCacheConcern
|
157
|
+
force_cache_class('foo')
|
158
|
+
end
|
159
|
+
class Foo7 < Foo6
|
160
|
+
force_cache_class('bar')
|
161
|
+
cache_version(6)
|
162
|
+
end
|
163
|
+
Foo7
|
164
|
+
end
|
165
|
+
|
166
|
+
it 'uses the prefers the forced class name from the macro' do
|
167
|
+
subject.expire_cache
|
168
|
+
expect(key_storage.store).to have_key(:'bar:v6:lmt')
|
169
|
+
end
|
170
|
+
end
|
116
171
|
end
|
117
172
|
|
118
173
|
context 'storage macros' do
|
@@ -40,6 +40,15 @@ describe 'LastModTimeKeyManager' do
|
|
40
40
|
expect(storage.store).to_not have_key(:'ns:lock')
|
41
41
|
end
|
42
42
|
|
43
|
+
it 'checks if the lock is present' do
|
44
|
+
subject.lock(req_keyspace, 100)
|
45
|
+
expect(subject.lock_present?(req_keyspace)).to eq(true)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'checks if the lock is not present' do
|
49
|
+
expect(subject.lock_present?(req_keyspace)).to eq(false)
|
50
|
+
end
|
51
|
+
|
43
52
|
it 'promotes a timestamp and last known key' do
|
44
53
|
subject.promote(req_keyspace, last_known_key: 'asdf', timestamp: timestamp)
|
45
54
|
expect(storage.read(:'ns:lkk')).to eq('asdf')
|
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.5.
|
4
|
+
version: 0.5.4.rc1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ibotta Developers
|
@@ -9,22 +9,22 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2021-
|
12
|
+
date: 2021-08-12 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: bundler
|
16
16
|
requirement: !ruby/object:Gem::Requirement
|
17
17
|
requirements:
|
18
|
-
- - "
|
18
|
+
- - ">="
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version: '
|
20
|
+
version: '0'
|
21
21
|
type: :development
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
|
-
- - "
|
25
|
+
- - ">="
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version: '
|
27
|
+
version: '0'
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
29
|
name: gems
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
@@ -137,6 +137,20 @@ dependencies:
|
|
137
137
|
- - "~>"
|
138
138
|
- !ruby/object:Gem::Version
|
139
139
|
version: 0.8.1
|
140
|
+
- !ruby/object:Gem::Dependency
|
141
|
+
name: pry
|
142
|
+
requirement: !ruby/object:Gem::Requirement
|
143
|
+
requirements:
|
144
|
+
- - ">="
|
145
|
+
- !ruby/object:Gem::Version
|
146
|
+
version: '0'
|
147
|
+
type: :development
|
148
|
+
prerelease: false
|
149
|
+
version_requirements: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
140
154
|
- !ruby/object:Gem::Dependency
|
141
155
|
name: activesupport
|
142
156
|
requirement: !ruby/object:Gem::Requirement
|
@@ -144,9 +158,6 @@ dependencies:
|
|
144
158
|
- - ">="
|
145
159
|
- !ruby/object:Gem::Version
|
146
160
|
version: '4.2'
|
147
|
-
- - "<"
|
148
|
-
- !ruby/object:Gem::Version
|
149
|
-
version: '6'
|
150
161
|
type: :runtime
|
151
162
|
prerelease: false
|
152
163
|
version_requirements: !ruby/object:Gem::Requirement
|
@@ -154,9 +165,6 @@ dependencies:
|
|
154
165
|
- - ">="
|
155
166
|
- !ruby/object:Gem::Version
|
156
167
|
version: '4.2'
|
157
|
-
- - "<"
|
158
|
-
- !ruby/object:Gem::Version
|
159
|
-
version: '6'
|
160
168
|
- !ruby/object:Gem::Dependency
|
161
169
|
name: murmurhash3
|
162
170
|
requirement: !ruby/object:Gem::Requirement
|