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