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 ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ bin/*
5
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in atomic_mem_cache_store.gemspec
4
+ gemspec
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,5 @@
1
+ require File.join(File.dirname(__FILE__),'..','atomic_mem_cache_store')
2
+
3
+ class AtomicMemCacheStore
4
+ VERSION = File.read(File.join(File.dirname(__FILE__),'..', '..','VERSION') ).strip
5
+ 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
@@ -0,0 +1,6 @@
1
+ begin
2
+ require 'spec/autorun'
3
+ rescue LoadError
4
+ require 'rspec'
5
+ end
6
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/atomic_mem_cache_store')
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