atomic_redis_cache 0.0.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/.gitignore ADDED
@@ -0,0 +1,10 @@
1
+ *.sw?
2
+
3
+ .ruby-version
4
+ .ruby-gemset
5
+ .bundle
6
+ Gemfile.lock
7
+
8
+ *.gem
9
+ pkg
10
+ vendor
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in atomic_redis_cache.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Anuj Das
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,58 @@
1
+ # AtomicRedisCache
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
11
+ - 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)
14
+
15
+ ## Installation
16
+
17
+ Add this line to your application's Gemfile:
18
+
19
+ gem 'atomic_redis_cache'
20
+
21
+ And then execute:
22
+
23
+ $ bundle
24
+
25
+ Or install it yourself as:
26
+
27
+ $ gem install atomic_redis_cache
28
+
29
+ ## Usage
30
+
31
+ Ensure that you set the value of `AtomicRedisCache.redis`. This can be either an
32
+ instance of redis-rb (or compatible), or a lambda/Proc evaluating to one. Ex.
33
+
34
+ ```
35
+ AtomicRedisCache.redis = Redis.new(:host => '127.0.0.1', :port => 6379)
36
+ ```
37
+
38
+ Then use it as you would Rails.cache:
39
+
40
+ ```
41
+ >> v = AtomicRedisCache.fetch('key') { Faraday.get('example.com').status }
42
+ => 200 # network call made
43
+ >> v = AtomicRedisCache.fetch('key') { }
44
+ => 200 # value read from cache
45
+ ```
46
+
47
+ There are a couple of configurable options can be set per fetch:
48
+ - `:expires_in` - expiry in seconds; defaults to a day
49
+ - `:race_condition_ttl` - time to lock down key for recalculation
50
+ - `:max_retries` - # of times to retry cache refresh before expiring
51
+
52
+ ## Contributing
53
+
54
+ 1. Fork it ( https://github.com/anujdas/atomic_redis_cache/fork )
55
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
56
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
57
+ 4. Push to the branch (`git push origin my-new-feature`)
58
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,26 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'atomic_redis_cache/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "atomic_redis_cache"
8
+ spec.version = AtomicRedisCache::VERSION
9
+ spec.authors = ["Anuj Das"]
10
+ spec.email = ["anujdas@gmail.com"]
11
+ spec.summary = %q{Use Redis as a multi-process atomic cache to avoid thundering herds and long calculations}
12
+ spec.description = %q{}
13
+ spec.homepage = "https://github.com/anujdas/atomic_redis_cache"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0")
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^spec/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency 'redis'
22
+
23
+ spec.add_development_dependency 'rake'
24
+ spec.add_development_dependency 'rspec', '~> 3.0'
25
+ spec.add_development_dependency 'fakeredis'
26
+ end
@@ -0,0 +1,3 @@
1
+ module AtomicRedisCache
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,61 @@
1
+ require 'atomic_redis_cache/version'
2
+
3
+ module AtomicRedisCache
4
+ DEFAULT_EXPIRATION = 60*60*24 # 86400 seconds in a day
5
+ DEFAULT_RACE_TTL = 30 # seconds to acquire new value
6
+ MAX_RETRIES = 3 # recalc attempts before expiring cache
7
+
8
+ class << self
9
+ attr_writer :redis
10
+
11
+ # Fetch from cache with fallback, just like ActiveSupport::Cache.
12
+ # The main differences are in the edge cases around expiration.
13
+ # - when cache expires, we avoid dogpile/thundering herd
14
+ # from multiple processes recalculating at once
15
+ # - when calculation takes too long (i.e., due to network traffic)
16
+ # we return the previously cached value for several attempts
17
+ # Options:
18
+ # :expires_in - expiry in seconds; defaults to a day
19
+ # :race_condition_ttl - time to lock value for recalculation
20
+ # :max_retries - # of times to retry cache refresh before expiring
21
+ def fetch(key, opts={}, &blk)
22
+ expires_in = opts[:expires_in] || DEFAULT_EXPIRATION
23
+ race_ttl = opts[:race_condition_ttl] || DEFAULT_RACE_TTL
24
+ retries = opts[:max_retries] || MAX_RETRIES
25
+
26
+ now = Time.now.to_i
27
+ ttl = expires_in + retries * race_ttl
28
+ t_key = "timer:#{key}"
29
+
30
+ if val = redis.get(key) # cache hit
31
+ if redis.get(t_key).to_i < now # expired entry or dne
32
+ redis.set t_key, now + race_ttl # block other callers for recalc duration
33
+ begin
34
+ Timeout.timeout(race_ttl) do # if recalc exceeds race_ttl, abort
35
+ val = Marshal.dump(blk.call) # determine new value
36
+ redis.multi do # atomically cache + mark as valid
37
+ redis.setex key, ttl, val
38
+ redis.set t_key, now + expires_in
39
+ end
40
+ end
41
+ rescue Timeout::Error => e # eval timed out, use cached val
42
+ end
43
+ end
44
+ else # cache miss
45
+ val = Marshal.dump(blk.call) # determine new value
46
+ redis.multi do # atomically cache + mark as valid
47
+ redis.setex key, ttl, val
48
+ redis.set t_key, now + expires_in
49
+ end
50
+ end
51
+
52
+ Marshal.load(val)
53
+ end
54
+
55
+ def redis
56
+ raise 'AtomicRedisCache.redis must be set before use.' unless @redis
57
+ @redis.respond_to?(:call) ? @redis.call : @redis
58
+ end
59
+ private :redis
60
+ end
61
+ end
@@ -0,0 +1,2 @@
1
+ require 'rspec'
2
+ require 'fakeredis/rspec'
File without changes
metadata ADDED
@@ -0,0 +1,123 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: atomic_redis_cache
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Anuj Das
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-08-11 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: rspec
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '3.0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: fakeredis
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ description: ''
79
+ email:
80
+ - anujdas@gmail.com
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - ".gitignore"
86
+ - Gemfile
87
+ - LICENSE.txt
88
+ - README.md
89
+ - Rakefile
90
+ - atomic_redis_cache.gemspec
91
+ - lib/atomic_redis_cache.rb
92
+ - lib/atomic_redis_cache/version.rb
93
+ - spec/spec_helper.rb
94
+ - spec/unit/.gitkeep
95
+ homepage: https://github.com/anujdas/atomic_redis_cache
96
+ licenses:
97
+ - MIT
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubyforge_project:
116
+ rubygems_version: 1.8.25
117
+ signing_key:
118
+ specification_version: 3
119
+ summary: Use Redis as a multi-process atomic cache to avoid thundering herds and long
120
+ calculations
121
+ test_files:
122
+ - spec/spec_helper.rb
123
+ - spec/unit/.gitkeep