atomic_mem_cache_store 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +66 -0
- data/Rakefile +22 -0
- data/VERSION +1 -0
- data/atomic_mem_cache_store.gemspec +24 -0
- data/lib/atomic_mem_cache_store.rb +48 -0
- data/lib/atomic_mem_cache_store/version.rb +5 -0
- data/spec/lib/atomic_mem_cache_store_spec.rb +83 -0
- data/spec/spec_helper.rb +6 -0
- metadata +118 -0
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
# AtomicMemCacheStore
|
2
|
+
|
3
|
+
## Why ?
|
4
|
+
|
5
|
+
Anyone caching slow content on a website with moderate to heavy traffic will
|
6
|
+
sooner of later be a victim of the [thundering herb issue](http://en.wikipedia.org/wiki/Thundering_herd_problem) also called Dog-pile Effect.
|
7
|
+
|
8
|
+
Basically cache invalidation of a high traffic page will trigger several concurrent cache recalculation that could lead to pikes of load and transient slow down of your architecture.
|
9
|
+
|
10
|
+
Rails (and any framework relying on active support cache store) does not offer any built-in solution to this problem. You have to make your own based on sweeper, cron and/or periodical cache warming up. This tend to explode your view/model logic across several files and technology and lead to maintenance/debugging nightmare.
|
11
|
+
|
12
|
+
## What ?
|
13
|
+
|
14
|
+
This gem is a drop-in alternative to active support memcache store that preserve the simplicity of your built-in framework cache methods (action, fragment, Rails.cache) while providing atomic cache invalidation and serving cold cache in the meantime.
|
15
|
+
|
16
|
+
Out of the box the cache calculation is made directly in the current process were cache has been invalidated for the first time. But you could override this cache store to support async cache calculation.
|
17
|
+
|
18
|
+
## How ?
|
19
|
+
|
20
|
+
Install the gem
|
21
|
+
|
22
|
+
gem install atomic_mem_cache_store
|
23
|
+
|
24
|
+
or add it to your Gemfile
|
25
|
+
|
26
|
+
gem 'atomic_mem_cache_store'
|
27
|
+
|
28
|
+
Then use it directly
|
29
|
+
|
30
|
+
cache = AtomicMemCacheStore.new
|
31
|
+
cache.write('key', 'value', :expires_in => 10)
|
32
|
+
cache.read('key')
|
33
|
+
|
34
|
+
Or for Rails add it in your config/environments/<env>.rb
|
35
|
+
|
36
|
+
config.cache_store = :atomic_mem_cache_store, %w( 127.0.0.1 ), { :namespace => "cache:#{Rails.env}" }
|
37
|
+
|
38
|
+
It supports the same parameters as [ActiveSupport::Cache::MemCacheStore](http://apidock.com/rails/ActiveSupport/Cache/MemCacheStore)
|
39
|
+
|
40
|
+
## When ?
|
41
|
+
|
42
|
+
- You are using memcached
|
43
|
+
- If you have high traffic or slow cache recalculation
|
44
|
+
- If you use fragment cache with expiry
|
45
|
+
- If you use Rails.cache.fetch with expiry
|
46
|
+
- If you use caching with expiry and trigger recalculation when you get an empty cache
|
47
|
+
|
48
|
+
## Drawbacks
|
49
|
+
|
50
|
+
- If you use only memcache cache without expiry, you won't benefit from any improvement as your cache invalidation will be due to LRU algorithm or manual sweeping. This will work as before though.
|
51
|
+
|
52
|
+
- This will not prevent your app from thundering herd effect due to LRU key sweeping as in this case the cache value is lost. This is not worse than what you have now, though.
|
53
|
+
|
54
|
+
- If you try to access key value directly be careful as you will bypass the atomicity mechanism. (this will work though even if the expiration of the key will be longer than expected).
|
55
|
+
|
56
|
+
## TL;DR
|
57
|
+
|
58
|
+
Basically unless you are doing weird stuff without your cache store, this should be a drop-in replacement with no real corner cases. Worse that can happen is more query to memcache, and slightly longer expiry on keys.
|
59
|
+
|
60
|
+
## License
|
61
|
+
|
62
|
+
MIT
|
63
|
+
|
64
|
+
## Copyright
|
65
|
+
|
66
|
+
Renaud Morvan (nel@w3fu.com)
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
|
3
|
+
begin
|
4
|
+
require "spec/rake/spectask" # RSpec 1.3
|
5
|
+
|
6
|
+
desc 'Run all specs in spec directory.'
|
7
|
+
Spec::Rake::SpecTask.new(:spec) do |task|
|
8
|
+
task.libs = ['lib', 'spec']
|
9
|
+
task.spec_files = FileList['spec/**/*_spec.rb']
|
10
|
+
end
|
11
|
+
rescue LoadError
|
12
|
+
require "rspec/core/rake_task" # RSpec 2.0
|
13
|
+
|
14
|
+
desc 'Run all specs in spec directory.'
|
15
|
+
RSpec::Core::RakeTask.new(:spec) do |t|
|
16
|
+
t.rspec_opts = %w{--colour --format progress}
|
17
|
+
t.pattern = 'spec/**/*_spec.rb'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
desc 'Default: runs specs.'
|
22
|
+
task :default => :spec
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "atomic_mem_cache_store/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "atomic_mem_cache_store"
|
7
|
+
s.version = AtomicMemCacheStore::VERSION
|
8
|
+
s.authors = ["Renaud (Nel) Morvan"]
|
9
|
+
s.email = ["nel@w3fu.com"]
|
10
|
+
s.homepage = "https://github.com/nel/atomic_mem_cache_store"
|
11
|
+
s.summary = %q{Rails memcached store with atomic expiration}
|
12
|
+
s.description = %q{Rails memcached store optimized for the thundering herb issue. This limit cache recalculation to a single process while as long as key is not swept by LRU. Drop-in replacement of Rails memcached store.}
|
13
|
+
|
14
|
+
s.rubyforge_project = "atomic_mem_cache_store"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_development_dependency "rspec"
|
22
|
+
s.add_runtime_dependency "activesupport", ">2.1"
|
23
|
+
s.add_runtime_dependency "memcache-client"
|
24
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
class AtomicMemCacheStore < ActiveSupport::Cache::MemCacheStore
|
4
|
+
NEWLY_STORED = "STORED\r\n"
|
5
|
+
|
6
|
+
class << self; attr_accessor :grace_period; end
|
7
|
+
@grace_period = 90
|
8
|
+
|
9
|
+
def read(key, options = nil)
|
10
|
+
result = super
|
11
|
+
|
12
|
+
if result.present?
|
13
|
+
timer_key = timer_key(key)
|
14
|
+
#check whether the cache is expired
|
15
|
+
if @data.get(timer_key, true).nil?
|
16
|
+
#optimistic lock to avoid concurrent recalculation
|
17
|
+
if @data.add(timer_key, '', self.class.grace_period, true) == NEWLY_STORED
|
18
|
+
#trigger cache recalculation
|
19
|
+
return handle_expired_read(key,result)
|
20
|
+
end
|
21
|
+
#already recalculated or expirated in another process/thread
|
22
|
+
end
|
23
|
+
#key not expired
|
24
|
+
end
|
25
|
+
result
|
26
|
+
end
|
27
|
+
|
28
|
+
def write(key, value, options = nil)
|
29
|
+
expiry = (options && options[:expires_in]) || 0
|
30
|
+
#extend write expiration period and reset expiration timer
|
31
|
+
options[:expires_in] = expiry + 2*self.class.grace_period unless expiry.zero?
|
32
|
+
@data.set(timer_key(key), '', expiry, true)
|
33
|
+
super
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
#to be overidden for something else than synchronous cache recalculation
|
39
|
+
def handle_expired_read(key,result)
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def timer_key(key)
|
46
|
+
"tk:#{key}"
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe AtomicMemCacheStore do
|
4
|
+
before(:all) do
|
5
|
+
begin
|
6
|
+
@store = AtomicMemCacheStore.new('127.0.0.1', :namespace => "spec-atomic")
|
7
|
+
@store.read('test')
|
8
|
+
rescue MemCache::MemCacheError
|
9
|
+
puts "You need a real memcache server to execute the specs, you can run those test on production server, this won't flush your memcache"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
before(:each) do
|
14
|
+
@seed = "#{Time.now.to_i}#{rand(1000000000000000000000000)}"
|
15
|
+
AtomicMemCacheStore.grace_period = 90
|
16
|
+
end
|
17
|
+
|
18
|
+
def prefix_key(value)
|
19
|
+
"#{@seed}#{value}"
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "with expiry" do
|
23
|
+
it "returns nil when key has not been set" do
|
24
|
+
@store.read(prefix_key('unknown')).should be_nil
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns value when key has been set and is not expired" do
|
28
|
+
key = prefix_key('not-expired')
|
29
|
+
@store.write(key, true, :expires_in => 10)
|
30
|
+
@store.read(key).should be
|
31
|
+
end
|
32
|
+
|
33
|
+
it "returns new value when key is rewriten" do
|
34
|
+
key = prefix_key('not-expired')
|
35
|
+
@store.write(key, 1, :expires_in => 10)
|
36
|
+
@store.write(key, 2, :expires_in => 10)
|
37
|
+
@store.read(key).should be 2
|
38
|
+
end
|
39
|
+
|
40
|
+
it "returns nil once when key is expired, and then the old value" do
|
41
|
+
key = prefix_key('expired')
|
42
|
+
|
43
|
+
@store.write(key, 1, :expires_in => 1)
|
44
|
+
sleep 2
|
45
|
+
@store.read(key).should be_nil
|
46
|
+
@store.read(key).should be 1
|
47
|
+
@store.read(key).should be 1
|
48
|
+
end
|
49
|
+
|
50
|
+
it "returns nil when 2 times the grace period is passed" do
|
51
|
+
AtomicMemCacheStore.grace_period = 1
|
52
|
+
key = prefix_key('expired')
|
53
|
+
|
54
|
+
@store.write(key, 1, :expires_in => 1)
|
55
|
+
sleep 2
|
56
|
+
@store.read(key).should be_nil
|
57
|
+
@store.read(key).should be 1
|
58
|
+
sleep 2
|
59
|
+
@store.read(key).should be_nil
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "without expiry" do
|
64
|
+
it "returns nil when key has not been set" do
|
65
|
+
@store.read(prefix_key('unknown')).should be_nil
|
66
|
+
end
|
67
|
+
|
68
|
+
it "returns value when key is set" do
|
69
|
+
key = prefix_key('key-without-expiry')
|
70
|
+
|
71
|
+
@store.write(key, 1)
|
72
|
+
@store.read(key).should be 1
|
73
|
+
end
|
74
|
+
|
75
|
+
it "returns new value when key is rewriten" do
|
76
|
+
key = prefix_key('key-without-expiry')
|
77
|
+
|
78
|
+
@store.write(key, 1)
|
79
|
+
@store.write(key, 2)
|
80
|
+
@store.read(key).should be 2
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: atomic_mem_cache_store
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Renaud (Nel) Morvan
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-01-26 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: rspec
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: activesupport
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
hash: 1
|
43
|
+
segments:
|
44
|
+
- 2
|
45
|
+
- 1
|
46
|
+
version: "2.1"
|
47
|
+
type: :runtime
|
48
|
+
version_requirements: *id002
|
49
|
+
- !ruby/object:Gem::Dependency
|
50
|
+
name: memcache-client
|
51
|
+
prerelease: false
|
52
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
53
|
+
none: false
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
hash: 3
|
58
|
+
segments:
|
59
|
+
- 0
|
60
|
+
version: "0"
|
61
|
+
type: :runtime
|
62
|
+
version_requirements: *id003
|
63
|
+
description: Rails memcached store optimized for the thundering herb issue. This limit cache recalculation to a single process while as long as key is not swept by LRU. Drop-in replacement of Rails memcached store.
|
64
|
+
email:
|
65
|
+
- nel@w3fu.com
|
66
|
+
executables: []
|
67
|
+
|
68
|
+
extensions: []
|
69
|
+
|
70
|
+
extra_rdoc_files: []
|
71
|
+
|
72
|
+
files:
|
73
|
+
- .gitignore
|
74
|
+
- Gemfile
|
75
|
+
- README.md
|
76
|
+
- Rakefile
|
77
|
+
- VERSION
|
78
|
+
- atomic_mem_cache_store.gemspec
|
79
|
+
- lib/atomic_mem_cache_store.rb
|
80
|
+
- lib/atomic_mem_cache_store/version.rb
|
81
|
+
- spec/lib/atomic_mem_cache_store_spec.rb
|
82
|
+
- spec/spec_helper.rb
|
83
|
+
homepage: https://github.com/nel/atomic_mem_cache_store
|
84
|
+
licenses: []
|
85
|
+
|
86
|
+
post_install_message:
|
87
|
+
rdoc_options: []
|
88
|
+
|
89
|
+
require_paths:
|
90
|
+
- lib
|
91
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
92
|
+
none: false
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
hash: 3
|
97
|
+
segments:
|
98
|
+
- 0
|
99
|
+
version: "0"
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
none: false
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
hash: 3
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
version: "0"
|
109
|
+
requirements: []
|
110
|
+
|
111
|
+
rubyforge_project: atomic_mem_cache_store
|
112
|
+
rubygems_version: 1.8.15
|
113
|
+
signing_key:
|
114
|
+
specification_version: 3
|
115
|
+
summary: Rails memcached store with atomic expiration
|
116
|
+
test_files:
|
117
|
+
- spec/lib/atomic_mem_cache_store_spec.rb
|
118
|
+
- spec/spec_helper.rb
|