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