atomic_redis_cache 0.2.0 → 0.2.1
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.
- data/README.md +14 -16
- data/atomic_redis_cache.gemspec +1 -0
- data/lib/atomic_redis_cache/version.rb +1 -1
- data/lib/atomic_redis_cache.rb +9 -3
- data/spec/spec_helper.rb +9 -0
- data/spec/unit/atomic_redis_cache_spec.rb +106 -0
- metadata +19 -3
- data/spec/unit/.gitkeep +0 -0
data/README.md
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
# AtomicRedisCache
|
|
2
2
|
|
|
3
|
-
AtomicRedisCache is an overlay on top of
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
- when
|
|
9
|
-
|
|
10
|
-
|
|
3
|
+
AtomicRedisCache is an overlay on top of Redis that allows you to use it much
|
|
4
|
+
like the Rails cache (ActiveSupport::Cache). Usage centers around the `.fetch()`
|
|
5
|
+
method, which reads from a key if present and valid, or else evaluates the
|
|
6
|
+
passed block and saves it as the new value at that key. The foremost design
|
|
7
|
+
goals apply mainly to webapps and are twofold:
|
|
8
|
+
- when the cached value expires, recalculate in the first process to access it,
|
|
9
|
+
serving the old value to all other processes until completion, thus avoiding
|
|
10
|
+
the dogpile/thundering herd effect
|
|
11
|
+
(http://en.wikipedia.org/wiki/Thundering_herd_problem)
|
|
11
12
|
- when calculation takes too long (i.e., due to db, network calls) and a
|
|
12
|
-
recently expired cached value is available,
|
|
13
|
-
calculating again later (for a
|
|
13
|
+
recently expired cached value is available, fail fast with it and try
|
|
14
|
+
calculating again later (for a reasonable # of retries)
|
|
14
15
|
|
|
15
16
|
## Installation
|
|
16
17
|
|
|
@@ -31,18 +32,15 @@ Or install it yourself as:
|
|
|
31
32
|
Ensure that you set the value of `AtomicRedisCache.redis`. This can be either an
|
|
32
33
|
instance of redis-rb (or compatible), or a lambda/Proc evaluating to one. Ex.
|
|
33
34
|
|
|
34
|
-
```
|
|
35
|
-
AtomicRedisCache.redis = Redis.new(:host => '127.0.0.1', :port => 6379)
|
|
36
|
-
```
|
|
35
|
+
``` AtomicRedisCache.redis = Redis.new(:host => '127.0.0.1', :port => 6379) ```
|
|
37
36
|
|
|
38
37
|
Then use it as you would Rails.cache:
|
|
39
38
|
|
|
40
39
|
```
|
|
41
40
|
>> v = AtomicRedisCache.fetch('key') { Faraday.get('example.com').status }
|
|
42
41
|
=> 200 # network call made
|
|
43
|
-
>> v = AtomicRedisCache.fetch('key') { }
|
|
44
|
-
=> 200 # value read from cache
|
|
45
|
-
```
|
|
42
|
+
>> v = AtomicRedisCache.fetch('key') { raise 'Network call attempted' }
|
|
43
|
+
=> 200 # value read from cache ```
|
|
46
44
|
|
|
47
45
|
There are a couple of configurable options can be set per fetch:
|
|
48
46
|
- `:expires_in` - expiry in seconds; defaults to a day
|
data/atomic_redis_cache.gemspec
CHANGED
data/lib/atomic_redis_cache.rb
CHANGED
|
@@ -52,11 +52,16 @@ module AtomicRedisCache
|
|
|
52
52
|
Marshal.load(val)
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# Fetch from the cache atomically; return nil if empty or expired
|
|
55
56
|
def read(key)
|
|
56
57
|
val, exp = redis.mget key, timer(key)
|
|
57
|
-
Marshal.
|
|
58
|
+
Marshal.load(val) unless exp.to_i < Time.now.to_i
|
|
58
59
|
end
|
|
59
60
|
|
|
61
|
+
# Write to the cache unconditionally, returns success as boolean
|
|
62
|
+
# Accepts the same options and uses the same defaults as .fetch()
|
|
63
|
+
# Note that write() ignores locks, so it can be called multiple times;
|
|
64
|
+
# prefer .fetch() unless absolutely necessary.
|
|
60
65
|
def write(key, val, opts={})
|
|
61
66
|
expires_in = opts[:expires_in] || DEFAULT_EXPIRATION
|
|
62
67
|
race_ttl = opts[:race_condition_ttl] || DEFAULT_RACE_TTL
|
|
@@ -72,8 +77,9 @@ module AtomicRedisCache
|
|
|
72
77
|
response.all? { |ret| ret == 'OK' }
|
|
73
78
|
end
|
|
74
79
|
|
|
80
|
+
# Delete the cache entry completely, including timer
|
|
75
81
|
def delete(key)
|
|
76
|
-
redis.del(key) ==
|
|
82
|
+
redis.del(key, timer(key)) == 2
|
|
77
83
|
end
|
|
78
84
|
|
|
79
85
|
def timer(key)
|
|
@@ -82,7 +88,7 @@ module AtomicRedisCache
|
|
|
82
88
|
private :timer
|
|
83
89
|
|
|
84
90
|
def redis
|
|
85
|
-
raise 'AtomicRedisCache.redis must be set
|
|
91
|
+
raise ArgumentError.new('AtomicRedisCache.redis must be set') unless @redis
|
|
86
92
|
@redis.respond_to?(:call) ? @redis.call : @redis
|
|
87
93
|
end
|
|
88
94
|
private :redis
|
data/spec/spec_helper.rb
CHANGED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require 'spec_helper'
|
|
2
|
+
|
|
3
|
+
describe AtomicRedisCache, :order => :random do
|
|
4
|
+
subject { AtomicRedisCache }
|
|
5
|
+
|
|
6
|
+
let(:redis) { Redis.new }
|
|
7
|
+
|
|
8
|
+
let(:key) { 'key' }
|
|
9
|
+
let(:val) { {:a => 1} }
|
|
10
|
+
let(:m_val) { Marshal.dump(val) }
|
|
11
|
+
let(:timer_key) { "timer:#{key}" }
|
|
12
|
+
let(:now) { Time.now.to_i }
|
|
13
|
+
|
|
14
|
+
before { subject.redis = redis }
|
|
15
|
+
|
|
16
|
+
describe '.redis=' do
|
|
17
|
+
it 'accepts a Redis instance' do
|
|
18
|
+
subject.redis = Redis.new
|
|
19
|
+
expect { subject.read(key) }.to_not raise_error
|
|
20
|
+
end
|
|
21
|
+
it 'accepts a lambda evaluating to a Redis instance' do
|
|
22
|
+
subject.redis = lambda { Redis.new }
|
|
23
|
+
expect { subject.read(key) }.to_not raise_error
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
describe '.fetch' do
|
|
28
|
+
it 'requires .redis to be set' do
|
|
29
|
+
subject.redis = nil
|
|
30
|
+
expect { subject.fetch(key) { val } }.to raise_error(ArgumentError)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
it 'serializes complex objects for storage' do
|
|
34
|
+
subject.fetch(key) { val }
|
|
35
|
+
expect(redis.get(key)).to eq(m_val)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'de-serializes objects on retrieval' do
|
|
39
|
+
redis.mset(key, m_val, timer_key, now)
|
|
40
|
+
expect(subject.fetch(key)).to eq(val)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it 'returns nil if a key does not exist' do
|
|
44
|
+
expect(subject.read(key)).to be_nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe '.read' do
|
|
49
|
+
it 'requires .redis to be set' do
|
|
50
|
+
subject.redis = nil
|
|
51
|
+
expect { subject.read(key) }.to raise_error(ArgumentError)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it 'de-serializes objects on retrieval' do
|
|
55
|
+
redis.mset(key, m_val, timer_key, now)
|
|
56
|
+
expect(subject.read(key)).to eq(val)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
it 'returns nil if a key does not exist' do
|
|
60
|
+
expect(subject.read(key)).to be_nil
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
it 'returns nil if a key is present but expired' do
|
|
64
|
+
redis.mset(key, m_val, timer_key, now)
|
|
65
|
+
expect(subject.read(key)).to eq(val)
|
|
66
|
+
Timecop.travel(now + 1) do
|
|
67
|
+
expect(subject.read(key)).to be_nil
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
describe '.write' do
|
|
73
|
+
it 'requires .redis to be set' do
|
|
74
|
+
subject.redis = nil
|
|
75
|
+
expect { subject.write(key, val) }.to raise_error(ArgumentError)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
it 'serializes complex objects for storage' do
|
|
79
|
+
subject.write(key, val)
|
|
80
|
+
expect(redis.get(key)).to eq(m_val)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe '.delete' do
|
|
85
|
+
it 'requires .redis to be set' do
|
|
86
|
+
subject.redis = nil
|
|
87
|
+
expect { subject.delete(key) }.to raise_error(ArgumentError)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it 'returns false if a key does not exist' do
|
|
91
|
+
expect(subject.delete(key)).to be_falsey
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
it 'returns true if a key exists' do
|
|
95
|
+
redis.mset(key, m_val, timer_key, now)
|
|
96
|
+
expect(subject.delete(key)).to be_truthy
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'deletes both the key and timer' do
|
|
100
|
+
redis.mset(key, m_val, timer_key, now)
|
|
101
|
+
subject.delete(key)
|
|
102
|
+
expect(redis.get(key)).to be_nil
|
|
103
|
+
expect(redis.get(timer_key)).to be_nil
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: atomic_redis_cache
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
prerelease:
|
|
6
6
|
platform: ruby
|
|
7
7
|
authors:
|
|
@@ -75,6 +75,22 @@ dependencies:
|
|
|
75
75
|
- - ">="
|
|
76
76
|
- !ruby/object:Gem::Version
|
|
77
77
|
version: '0'
|
|
78
|
+
- !ruby/object:Gem::Dependency
|
|
79
|
+
name: timecop
|
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
|
81
|
+
none: false
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
type: :development
|
|
87
|
+
prerelease: false
|
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
89
|
+
none: false
|
|
90
|
+
requirements:
|
|
91
|
+
- - ">="
|
|
92
|
+
- !ruby/object:Gem::Version
|
|
93
|
+
version: '0'
|
|
78
94
|
description: ''
|
|
79
95
|
email:
|
|
80
96
|
- anujdas@gmail.com
|
|
@@ -91,7 +107,7 @@ files:
|
|
|
91
107
|
- lib/atomic_redis_cache.rb
|
|
92
108
|
- lib/atomic_redis_cache/version.rb
|
|
93
109
|
- spec/spec_helper.rb
|
|
94
|
-
- spec/unit
|
|
110
|
+
- spec/unit/atomic_redis_cache_spec.rb
|
|
95
111
|
homepage: https://github.com/anujdas/atomic_redis_cache
|
|
96
112
|
licenses:
|
|
97
113
|
- MIT
|
|
@@ -120,4 +136,4 @@ summary: Use Redis as a multi-process atomic cache to avoid thundering herds and
|
|
|
120
136
|
calculations
|
|
121
137
|
test_files:
|
|
122
138
|
- spec/spec_helper.rb
|
|
123
|
-
- spec/unit
|
|
139
|
+
- spec/unit/atomic_redis_cache_spec.rb
|
data/spec/unit/.gitkeep
DELETED
|
File without changes
|