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 CHANGED
@@ -1,16 +1,17 @@
1
1
  # AtomicRedisCache
2
2
 
3
- AtomicRedisCache is an overlay on top of the redis-rb library that allows you to
4
- use Redis much like the Rails cache (ActiveSupport::Cache). It duplicates only
5
- the .fetch() method, which reads from a key if present and valid, or else
6
- evaluates the passed block and saves it as the new value at that key. The
7
- foremost design goals apply mainly to webapps and are twofold:
8
- - when cache expires, to avoid dogpile/thundering herd from multiple processes
9
- recalculating by serving the cached value to all but one process, then updating
10
- atomically when calculation by that process completes
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, to fail fast with it and try
13
- calculating again later (for a reaosnable # of retries)
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
@@ -23,4 +23,5 @@ Gem::Specification.new do |spec|
23
23
  spec.add_development_dependency 'rake'
24
24
  spec.add_development_dependency 'rspec', '~> 3.0'
25
25
  spec.add_development_dependency 'fakeredis'
26
+ spec.add_development_dependency 'timecop'
26
27
  end
@@ -1,3 +1,3 @@
1
1
  module AtomicRedisCache
2
- VERSION = '0.2.0'
2
+ VERSION = '0.2.1'
3
3
  end
@@ -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.dump(val) if exp > Time.now.to_i
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) == 1
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 before use.' unless @redis
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
@@ -1,2 +1,11 @@
1
1
  require 'rspec'
2
2
  require 'fakeredis/rspec'
3
+ require 'timecop'
4
+
5
+ require 'atomic_redis_cache'
6
+
7
+ RSpec.configure do |config|
8
+ config.around(:each) do |example|
9
+ Timecop.freeze(Time.now.utc, &example)
10
+ end
11
+ end
@@ -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.0
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/.gitkeep
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/.gitkeep
139
+ - spec/unit/atomic_redis_cache_spec.rb
data/spec/unit/.gitkeep DELETED
File without changes