rails3_libmemcached_store 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ *.swp
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ gemfile:
5
+ - gemfiles/rails30.gemfile
6
+ - gemfiles/rails31.gemfile
7
+ - gemfiles/rails32.gemfile
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008 37signals
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,66 @@
1
+ # LibmemcachedStore
2
+
3
+ An ActiveSupport cache store that uses the C-based libmemcached client through Evan Weaver's Ruby/SWIG wrapper, [memcached](https://github.com/evan/memcached). libmemcached is fast (fastest memcache client for Ruby), lightweight, and supports consistent hashing, non-blocking IO, and graceful server failover.
4
+
5
+ This cache is designed for Rails 3+ applications.
6
+
7
+ ## Prerequisites
8
+
9
+ You'll need the memcached gem installed:
10
+
11
+ ```ruby
12
+ gem install memcached
13
+ ```
14
+
15
+ or in your Gemfile
16
+
17
+ ```ruby
18
+ gem 'memcached'
19
+ ```
20
+
21
+ There are no other dependencies.
22
+
23
+ ## Installation
24
+
25
+ Just add to your Gemfile
26
+
27
+ ```ruby
28
+ gem 'rails3_libmemcached_store'
29
+ ```
30
+
31
+ and you're set.
32
+
33
+ ## Usage
34
+
35
+ This is a drop-in replacement for the memcache store that ships with Rails. To
36
+ enable, set the `config.cache_store` option to `libmemcached_store`
37
+ in the config for your environment
38
+
39
+ ```ruby
40
+ config.cache_store = :libmemcached_store
41
+ ```
42
+
43
+ If no servers are specified, localhost is assumed. You can specify a list of
44
+ server addresses, either as hostnames or IP addresses, with or without a port
45
+ designation. If no port is given, 11211 is assumed:
46
+
47
+ ```ruby
48
+ config.cache_store = :libmemcached_store, %w(cache-01 cache-02 127.0.0.1:11212)
49
+ ```
50
+
51
+ Other options are passed directly to the memcached client
52
+
53
+ ```ruby
54
+ config.cache_store = :libmemcached_store, '127.0.0.1:11211', :default_ttl => 3600, :compress => true
55
+ ```
56
+
57
+ You can also use `:libmemcached_store` to store your application sessions
58
+
59
+ ```ruby
60
+ config.session_store = :libmemcached_store, :namespace => '_session', :expire_after => 1800
61
+ ```
62
+
63
+ ## Props
64
+
65
+ Thanks to Brian Aker ([http://tangent.org](http://tangent.org)) for creating libmemcached, and Evan
66
+ Weaver ([http://blog.evanweaver.com](http://blog.evanweaver.com)) for the Ruby wrapper.
data/Rakefile ADDED
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'rake'
5
+ require 'rake/testtask'
6
+ require 'rdoc/task'
7
+
8
+ task :default => :test
9
+
10
+ Rake::TestTask.new do |t|
11
+ t.libs << 'test'
12
+ t.pattern = 'test/**/*_test.rb'
13
+ t.warning = false
14
+ t.verbose = true
15
+ end
16
+
17
+ desc 'Generate documentation for the libmemcached_store plugin.'
18
+ Rake::RDocTask.new(:rdoc) do |rdoc|
19
+ rdoc.rdoc_dir = 'rdoc'
20
+ rdoc.title = 'LibmemcachedStore'
21
+ rdoc.options << '--line-numbers' << '--inline-source'
22
+ rdoc.rdoc_files.include('README')
23
+ rdoc.rdoc_files.include('lib/**/*.rb')
24
+ end
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'activesupport', '~> 3.0.0'
4
+ gem 'actionpack', '~> 3.0.0'
5
+ gem 'mocha'
6
+
7
+ gemspec :path => '../'
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'activesupport', '~> 3.1.0'
4
+ gem 'actionpack', '~> 3.1.0'
5
+ gem 'mocha'
6
+
7
+ gemspec :path => '../'
@@ -0,0 +1,7 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem 'activesupport', '~> 3.2.0'
4
+ gem 'actionpack', '~> 3.2.0'
5
+ gem 'mocha'
6
+
7
+ gemspec :path => '../'
@@ -0,0 +1,81 @@
1
+ require 'memcached'
2
+ require 'rack/session/abstract/id'
3
+
4
+ module ActionDispatch
5
+ module Session
6
+ class LibmemcachedStore < AbstractStore
7
+
8
+ DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS.merge(:prefix_key => 'rack:session', :memcache_server => 'localhost:11211')
9
+
10
+ def initialize(app, options = {})
11
+ options[:expire_after] ||= options[:expires]
12
+ super
13
+ @mutex = Mutex.new
14
+ @pool = options[:cache] || Memcached.new(@default_options[:memcache_server], @default_options)
15
+ end
16
+
17
+ private
18
+
19
+ def generate_sid
20
+ loop do
21
+ sid = super
22
+ begin
23
+ @pool.get(sid)
24
+ rescue Memcached::NotFound
25
+ break sid
26
+ end
27
+ end
28
+ end
29
+
30
+ def get_session(env, sid)
31
+ sid ||= generate_sid
32
+ session = with_lock(env, {}) do
33
+ begin
34
+ @pool.get(sid)
35
+ rescue Memcached::NotFound
36
+ {}
37
+ end
38
+ end
39
+ [sid, session]
40
+ end
41
+
42
+ def set_session(env, session_id, new_session, options = {})
43
+ expiry = options[:expire_after]
44
+ expiry = expiry.nil? ? 0 : expiry + 1
45
+
46
+ with_lock(env, false) do
47
+ @pool.set(session_id, new_session, expiry)
48
+ session_id
49
+ end
50
+ end
51
+
52
+ def destroy_session(env, session_id, options = {})
53
+ with_lock(env, nil) do
54
+ @pool.delete(session_id)
55
+ generate_sid unless options[:drop]
56
+ end
57
+ end
58
+
59
+ #
60
+ # Deprecated since Rails 3.1.0
61
+ #
62
+ def destroy(env)
63
+ if sid = current_session_id(env)
64
+ with_lock(env, false) do
65
+ @pool.delete(sid)
66
+ end
67
+ end
68
+ end
69
+
70
+ def with_lock(env, default)
71
+ @mutex.lock if env['rack.multithread']
72
+ yield
73
+ rescue Memcached::Error => e
74
+ default
75
+ ensure
76
+ @mutex.unlock if @mutex.locked?
77
+ end
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveSupport
2
+ module Cache
3
+ class CompressedLibmemcachedStore < LibmemcachedStore
4
+ def read(name, options = {})
5
+ if value = super(name, (options || {}).merge(:raw => true))
6
+ Marshal.load(ActiveSupport::Gzip.decompress(value))
7
+ end
8
+ end
9
+
10
+ def write(name, value, options = {})
11
+ super(name, ActiveSupport::Gzip.compress(Marshal.dump(value)), (options || {}).merge(:raw => true))
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,135 @@
1
+ require 'memcached'
2
+
3
+ class ActiveSupport::Cache::Entry
4
+ # In 3.0 all values returned from Rails.cache.read are frozen.
5
+ # This makes sense for an in-memory store storing object references,
6
+ # but for a marshalled store we should be able to modify things.
7
+ # Starting with 3.2, values are not frozen anymore.
8
+ def value_with_dup
9
+ result = value_without_dup
10
+ result.frozen? && result.duplicable? ? result.dup : result
11
+ end
12
+ alias_method_chain :value, :dup
13
+ end
14
+
15
+ module ActiveSupport
16
+ module Cache
17
+ class LibmemcachedStore < Store
18
+ attr_reader :addresses
19
+
20
+ DEFAULT_OPTIONS = {
21
+ :distribution => :consistent_ketama,
22
+ :binary_protocol => true
23
+ }
24
+
25
+ def initialize(*addresses)
26
+ addresses.flatten!
27
+ @options = addresses.extract_options!
28
+ @addresses = addresses
29
+ @cache = Memcached.new(@addresses, @options.reverse_merge(DEFAULT_OPTIONS))
30
+ end
31
+
32
+ def increment(key, amount = 1, options = nil)
33
+ instrument(:increment, key, amount: amount) do
34
+ @cache.incr(key, amount)
35
+ end
36
+ rescue Memcached::Error
37
+ nil
38
+ end
39
+
40
+ def decrement(key, amount = 1, options = nil)
41
+ instrument(:decrement, key, amount: amount) do
42
+ @cache.decr(key, amount)
43
+ end
44
+ rescue Memcached::Error
45
+ nil
46
+ end
47
+
48
+ #
49
+ # Optimize read_multi to only make one call to memcached
50
+ # server.
51
+ #
52
+ def read_multi(*names)
53
+ options = names.extract_options!
54
+ options = merged_options(options)
55
+
56
+ return {} if names.empty?
57
+
58
+ keys_to_names = Hash[names.map {|name| [namespaced_key(name, options), name] }]
59
+ raw_values = @cache.get(keys_to_names.keys, false)
60
+
61
+ values = {}
62
+ raw_values.each do |key, value|
63
+ entry = deserialize_entry(value)
64
+ values[keys_to_names[key]] = entry.value unless entry.expired?
65
+ end
66
+ values
67
+ end
68
+
69
+ def clear
70
+ @cache.flush
71
+ end
72
+
73
+ def stats
74
+ @cache.stats
75
+ end
76
+
77
+ protected
78
+
79
+ def read_entry(key, options = nil)
80
+ deserialize_entry(@cache.get(key, false))
81
+ rescue Memcached::NotFound
82
+ nil
83
+ rescue Memcached::Error => e
84
+ log_error(e)
85
+ nil
86
+ end
87
+
88
+ # Set the key to the given value. Pass :unless_exist => true if you want to
89
+ # skip setting a key that already exists.
90
+ def write_entry(key, entry, options = nil)
91
+ method = (options && options[:unless_exist]) ? :add : :set
92
+ value = options[:raw] ? entry.value.to_s : entry
93
+
94
+ @cache.send(method, key, value, expires_in(options), marshal?(options))
95
+ true
96
+ rescue Memcached::Error => e
97
+ log_error(e)
98
+ false
99
+ end
100
+
101
+ def delete_entry(key, options = nil)
102
+ @cache.delete(key)
103
+ true
104
+ rescue Memcached::NotFound
105
+ false
106
+ rescue Memcached::Error => e
107
+ log_error(e)
108
+ false
109
+ end
110
+
111
+ private
112
+ def deserialize_entry(raw_value)
113
+ if raw_value
114
+ entry = Marshal.load(raw_value) rescue raw_value
115
+ entry.is_a?(Entry) ? entry : Entry.new(entry)
116
+ else
117
+ nil
118
+ end
119
+ end
120
+
121
+ def expires_in(options)
122
+ (options || {})[:expires_in].to_i
123
+ end
124
+
125
+ def marshal?(options)
126
+ !(options || {})[:raw]
127
+ end
128
+
129
+ def log_error(exception)
130
+ return unless logger && logger.error?
131
+ logger.error "MemcachedError (#{exception.inspect}): #{exception.message}"
132
+ end
133
+ end
134
+ end
135
+ end
@@ -0,0 +1,3 @@
1
+ require 'active_support/cache/libmemcached_store'
2
+ require 'active_support/cache/compressed_libmemcached_store'
3
+ require 'action_dispatch/session/libmemcached_store'
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ module LibmemcachedStore
2
+ VERSION = "0.4.0".freeze
3
+ end
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "rails3_libmemcached_store"
7
+ s.version = LibmemcachedStore::VERSION.dup
8
+ s.platform = Gem::Platform::RUBY
9
+ s.summary = "ActiveSupport 3+ cache store for the C-based libmemcached client"
10
+ s.email = "cocchi.c@gmail.com"
11
+ s.homepage = "http://github.com/ccocchi/libmemcached_store"
12
+ s.description = %q{An ActiveSupport cache store that uses the C-based libmemcached client through
13
+ Evan Weaver's Ruby/SWIG wrapper, memcached. libmemcached is fast, lightweight,
14
+ and supports consistent hashing, non-blocking IO, and graceful server failover.}
15
+ s.authors = ["Christopher Cocchi-Perrier", "Ben Hutton", "Jeffrey Hardy"]
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency("memcached", ">= 0")
23
+
24
+ s.add_development_dependency('rack')
25
+ s.add_development_dependency('rake')
26
+ s.add_development_dependency('mocha')
27
+ s.add_development_dependency('activesupport', '>= 3')
28
+ s.add_development_dependency('actionpack', '>= 3')
29
+ end
30
+
@@ -0,0 +1,43 @@
1
+ require 'active_support'
2
+ require 'active_support/core_ext/module/delegation'
3
+ require 'active_support/core_ext/module/attribute_accessors'
4
+ require 'action_controller'
5
+ require 'action_dispatch/routing'
6
+ require 'action_dispatch/middleware/head'
7
+ require 'action_dispatch/testing/assertions'
8
+ require 'action_dispatch/testing/test_process'
9
+ require 'action_dispatch/testing/integration'
10
+
11
+ SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
12
+
13
+ class RoutedRackApp
14
+ attr_reader :routes
15
+
16
+ def initialize(routes, &blk)
17
+ @routes = routes
18
+ @stack = ActionDispatch::MiddlewareStack.new(&blk).build(@routes)
19
+ end
20
+
21
+ def call(env)
22
+ @stack.call(env)
23
+ end
24
+ end
25
+
26
+ class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
27
+ def self.build_app(routes = nil)
28
+ RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
29
+ if defined?(ActionDispatch::PublicExceptions)
30
+ middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("/dev/null")
31
+ middleware.use "ActionDispatch::DebugExceptions"
32
+ else
33
+ middleware.use "ActionDispatch::ShowExceptions"
34
+ end
35
+ middleware.use "ActionDispatch::Callbacks"
36
+ middleware.use "ActionDispatch::ParamsParser"
37
+ middleware.use "ActionDispatch::Cookies"
38
+ middleware.use "ActionDispatch::Flash"
39
+ middleware.use "ActionDispatch::Head"
40
+ yield(middleware) if block_given?
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,200 @@
1
+ require 'test_helper'
2
+ require File.expand_path('../abstract_unit', __FILE__)
3
+ require 'action_dispatch/session/libmemcached_store'
4
+
5
+ class LibMemcachedStoreTest < ActionDispatch::IntegrationTest
6
+ class TestController < ActionController::Base
7
+ def no_session_access
8
+ head :ok
9
+ end
10
+
11
+ def set_session_value
12
+ session[:foo] = "bar"
13
+ head :ok
14
+ end
15
+
16
+ def set_serialized_session_value
17
+ session[:foo] = SessionAutoloadTest::Foo.new
18
+ head :ok
19
+ end
20
+
21
+ def get_session_value
22
+ render :text => "foo: #{session[:foo].inspect}"
23
+ end
24
+
25
+ def get_session_id
26
+ render :text => "#{request.session_options[:id]}"
27
+ end
28
+
29
+ def call_reset_session
30
+ session[:bar]
31
+ reset_session
32
+ session[:bar] = "baz"
33
+ head :ok
34
+ end
35
+
36
+ def self._routes
37
+ SharedTestRoutes
38
+ end
39
+ end
40
+
41
+ def test_setting_and_getting_session_value
42
+ with_test_route_set do
43
+ get '/set_session_value'
44
+ assert_response :success
45
+ assert cookies['_session_id']
46
+
47
+ get '/get_session_value'
48
+ assert_response :success
49
+ assert_equal 'foo: "bar"', response.body
50
+ end
51
+ end
52
+
53
+ def test_getting_nil_session_value
54
+ with_test_route_set do
55
+ get '/get_session_value'
56
+ assert_response :success
57
+ assert_equal 'foo: nil', response.body
58
+ end
59
+ end
60
+
61
+ def test_getting_session_value_after_session_reset
62
+ with_test_route_set do
63
+ get '/set_session_value'
64
+ assert_response :success
65
+ assert cookies['_session_id']
66
+ session_cookie = cookies.send(:hash_for)['_session_id']
67
+
68
+ get '/call_reset_session'
69
+ assert_response :success
70
+ assert_not_equal [], headers['Set-Cookie']
71
+
72
+ cookies << session_cookie # replace our new session_id with our old, pre-reset session_id
73
+
74
+ get '/get_session_value'
75
+ assert_response :success
76
+ assert_equal 'foo: nil', response.body, "data for this session should have been obliterated from memcached"
77
+ end
78
+ end
79
+
80
+ def test_getting_from_nonexistent_session
81
+ with_test_route_set do
82
+ get '/get_session_value'
83
+ assert_response :success
84
+ assert_equal 'foo: nil', response.body
85
+ assert_nil cookies['_session_id'], "should only create session on write, not read"
86
+ end
87
+ end
88
+
89
+ def test_setting_session_value_after_session_reset
90
+ with_test_route_set do
91
+ get '/set_session_value'
92
+ assert_response :success
93
+ assert cookies['_session_id']
94
+ session_id = cookies['_session_id']
95
+
96
+ get '/call_reset_session'
97
+ assert_response :success
98
+ assert_not_equal [], headers['Set-Cookie']
99
+
100
+ get '/get_session_value'
101
+ assert_response :success
102
+ assert_equal 'foo: nil', response.body
103
+
104
+ get '/get_session_id'
105
+ assert_response :success
106
+ assert_not_equal session_id, response.body
107
+ end
108
+ end
109
+
110
+ def test_getting_session_id
111
+ with_test_route_set do
112
+ get '/set_session_value'
113
+ assert_response :success
114
+ assert cookies['_session_id']
115
+ session_id = cookies['_session_id']
116
+
117
+ get '/get_session_id'
118
+ assert_response :success
119
+ assert_equal session_id, response.body, "should be able to read session id without accessing the session hash"
120
+ end
121
+ end
122
+
123
+ def test_deserializes_unloaded_class
124
+ with_test_route_set do
125
+ with_autoload_path "session_autoload_test" do
126
+ get '/set_serialized_session_value'
127
+ assert_response :success
128
+ assert cookies['_session_id']
129
+ end
130
+ with_autoload_path "session_autoload_test" do
131
+ get '/get_session_id'
132
+ assert_response :success
133
+ end
134
+ with_autoload_path "session_autoload_test" do
135
+ get '/get_session_value'
136
+ assert_response :success
137
+ assert_equal 'foo: #<SessionAutoloadTest::Foo bar:"baz">', response.body, "should auto-load unloaded class"
138
+ end
139
+ end
140
+ end
141
+
142
+ def test_doesnt_write_session_cookie_if_session_id_is_already_exists
143
+ with_test_route_set do
144
+ get '/set_session_value'
145
+ assert_response :success
146
+ assert cookies['_session_id']
147
+
148
+ get '/get_session_value'
149
+ assert_response :success
150
+ assert_equal nil, headers['Set-Cookie'], "should not resend the cookie again if session_id cookie is already exists"
151
+ end
152
+ end
153
+
154
+ def test_prevents_session_fixation
155
+ with_test_route_set do
156
+ get '/get_session_value'
157
+ assert_response :success
158
+ assert_equal 'foo: nil', response.body
159
+ session_id = cookies['_session_id']
160
+
161
+ reset!
162
+
163
+ get '/set_session_value', :_session_id => session_id
164
+ assert_response :success
165
+ assert_not_equal session_id, cookies['_session_id']
166
+ end
167
+ end
168
+
169
+ private
170
+
171
+ def with_test_route_set
172
+ with_routing do |set|
173
+ set.draw do
174
+ match ':action', :to => TestController
175
+ end
176
+
177
+ @app = self.class.build_app(set) do |middleware|
178
+ middleware.use ActionDispatch::Session::LibmemcachedStore, :key => '_session_id'
179
+ middleware.delete "ActionDispatch::ShowExceptions"
180
+ end
181
+
182
+ yield
183
+ end
184
+ end
185
+
186
+ def with_autoload_path(path)
187
+ path = File.join(File.dirname(__FILE__), "../fixtures", path)
188
+ if ActiveSupport::Dependencies.autoload_paths.include?(path)
189
+ yield
190
+ else
191
+ begin
192
+ ActiveSupport::Dependencies.autoload_paths << path
193
+ yield
194
+ ensure
195
+ ActiveSupport::Dependencies.autoload_paths.reject! {|p| p == path}
196
+ ActiveSupport::Dependencies.clear
197
+ end
198
+ end
199
+ end
200
+ end
@@ -0,0 +1,351 @@
1
+ require 'test_helper'
2
+ require 'memcached'
3
+ require 'active_support'
4
+ require 'active_support/core_ext/module/aliasing'
5
+ require 'active_support/core_ext/object/duplicable'
6
+ require 'active_support/cache/libmemcached_store'
7
+
8
+ # Make it easier to get at the underlying cache options during testing.
9
+ class ActiveSupport::Cache::LibmemcachedStore
10
+ delegate :options, :to => '@cache'
11
+ end
12
+
13
+ module CacheStoreBehavior
14
+ def test_should_read_and_write_strings
15
+ assert_equal true, @cache.write('foo', 'bar')
16
+ assert_equal 'bar', @cache.read('foo')
17
+ end
18
+
19
+ def test_should_overwrite
20
+ @cache.write('foo', 'bar')
21
+ @cache.write('foo', 'baz')
22
+ assert_equal 'baz', @cache.read('foo')
23
+ end
24
+
25
+ def test_fetch_without_cache_miss
26
+ @cache.write('foo', 'bar')
27
+ @cache.expects(:write).never
28
+ assert_equal 'bar', @cache.fetch('foo') { 'baz' }
29
+ end
30
+
31
+ def test_fetch_with_cache_miss
32
+ @cache.expects(:write).with('foo', 'baz', @cache.options)
33
+ assert_equal 'baz', @cache.fetch('foo') { 'baz' }
34
+ end
35
+
36
+ def test_fetch_with_forced_cache_miss
37
+ @cache.write('foo', 'bar')
38
+ @cache.expects(:read).never
39
+ @cache.expects(:write).with('foo', 'bar', @cache.options.merge(:force => true))
40
+ @cache.fetch('foo', :force => true) { 'bar' }
41
+ end
42
+
43
+ def test_fetch_with_cached_nil
44
+ @cache.write('foo', nil)
45
+ @cache.expects(:write).never
46
+ assert_nil @cache.fetch('foo') { 'baz' }
47
+ end
48
+
49
+ def test_should_read_and_write_hash
50
+ assert_equal true, @cache.write('foo', {:a => "b"})
51
+ assert_equal({:a => "b"}, @cache.read('foo'))
52
+ end
53
+
54
+ def test_should_read_and_write_integer
55
+ assert_equal true, @cache.write('foo', 1)
56
+ assert_equal 1, @cache.read('foo')
57
+ end
58
+
59
+ def test_should_read_and_write_nil
60
+ assert_equal true, @cache.write('foo', nil)
61
+ assert_equal nil, @cache.read('foo')
62
+ end
63
+
64
+ def test_should_read_and_write_false
65
+ assert @cache.write('foo', false)
66
+ if ActiveSupport::VERSION::MAJOR == 3 && ActiveSupport::VERSION::MINOR == 0
67
+ assert_equal nil, @cache.read('foo')
68
+ else
69
+ assert_equal false, @cache.read('foo')
70
+ end
71
+ end
72
+
73
+ def test_read_multi
74
+ @cache.write('foo', 'bar')
75
+ @cache.write('fu', 'baz')
76
+ @cache.write('fud', 'biz')
77
+ assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi('foo', 'fu'))
78
+ end
79
+
80
+ def test_read_multi_with_expires
81
+ @cache.write('foo', 'bar', :expires_in => 0.001)
82
+ @cache.write('fu', 'baz')
83
+ @cache.write('fud', 'biz')
84
+ sleep(0.002)
85
+ assert_equal({"fu" => "baz"}, @cache.read_multi('foo', 'fu'))
86
+ end
87
+
88
+ def test_read_and_write_compressed_small_data
89
+ @cache.write('foo', 'bar', :compress => true)
90
+ raw_value = @cache.send(:read_entry, 'foo', {}).raw_value
91
+ assert_equal 'bar', @cache.read('foo')
92
+ value = Marshal.load(raw_value) rescue raw_value
93
+ assert_equal 'bar', value
94
+ end
95
+
96
+ def test_read_and_write_compressed_large_data
97
+ @cache.write('foo', 'bar', :compress => true, :compress_threshold => 2)
98
+ raw_value = @cache.send(:read_entry, 'foo', {}).raw_value
99
+ assert_equal 'bar', @cache.read('foo')
100
+ assert_equal 'bar', Marshal.load(Zlib::Inflate.inflate(raw_value))
101
+ end
102
+
103
+ def test_read_and_write_compressed_nil
104
+ @cache.write('foo', nil, :compress => true)
105
+ assert_nil @cache.read('foo')
106
+ end
107
+
108
+ def test_cache_key
109
+ obj = Object.new
110
+ def obj.cache_key
111
+ :foo
112
+ end
113
+ @cache.write(obj, "bar")
114
+ assert_equal "bar", @cache.read("foo")
115
+ end
116
+
117
+ def test_param_as_cache_key
118
+ obj = Object.new
119
+ def obj.to_param
120
+ "foo"
121
+ end
122
+ @cache.write(obj, "bar")
123
+ assert_equal "bar", @cache.read("foo")
124
+ end
125
+
126
+ def test_array_as_cache_key
127
+ @cache.write([:fu, "foo"], "bar")
128
+ assert_equal "bar", @cache.read("fu/foo")
129
+ end
130
+
131
+ def test_hash_as_cache_key
132
+ @cache.write({:foo => 1, :fu => 2}, "bar")
133
+ assert_equal "bar", @cache.read("foo=1/fu=2")
134
+ end
135
+
136
+ def test_keys_are_case_sensitive
137
+ @cache.write("foo", "bar")
138
+ assert_nil @cache.read("FOO")
139
+ end
140
+
141
+ def test_exist
142
+ @cache.write('foo', 'bar')
143
+ assert_equal true, @cache.exist?('foo')
144
+ assert_equal false, @cache.exist?('bar')
145
+ end
146
+
147
+ def test_nil_exist
148
+ @cache.write('foo', nil)
149
+ assert_equal true, @cache.exist?('foo')
150
+ end
151
+
152
+ def test_delete
153
+ @cache.write('foo', 'bar')
154
+ assert @cache.exist?('foo')
155
+ assert_equal true, @cache.delete('foo')
156
+ assert !@cache.exist?('foo')
157
+ end
158
+
159
+ def test_delete_with_unexistent_key
160
+ @cache.expects(:log_error).never
161
+ assert !@cache.exist?('foo')
162
+ assert_equal false, @cache.delete('foo')
163
+ end
164
+
165
+ def test_read_should_return_a_different_object_id_each_time_it_is_called
166
+ @cache.write('foo', 'bar')
167
+ refute_equal @cache.read('foo').object_id, @cache.read('foo').object_id
168
+ end
169
+
170
+ def test_store_objects_should_be_immutable
171
+ @cache.write('foo', 'bar')
172
+ @cache.read('foo').gsub!(/.*/, 'baz')
173
+ assert_equal 'bar', @cache.read('foo')
174
+ end
175
+
176
+ def test_original_store_objects_should_not_be_immutable
177
+ bar = 'bar'
178
+ @cache.write('foo', bar)
179
+ assert_equal 'baz', bar.gsub!(/r/, 'z')
180
+ end
181
+
182
+ def test_expires_in
183
+ time = Time.local(2008, 4, 24)
184
+ Time.stubs(:now).returns(time)
185
+
186
+ @cache.write('foo', 'bar', :expires_in => 45)
187
+ assert_equal 'bar', @cache.read('foo')
188
+
189
+ Time.stubs(:now).returns(time + 30)
190
+ assert_equal 'bar', @cache.read('foo')
191
+
192
+ Time.stubs(:now).returns(time + 61)
193
+ assert_nil @cache.read('foo')
194
+ end
195
+
196
+ def test_expires_in_as_activesupport_duration
197
+ time = Time.local(2012, 02, 03)
198
+ Time.stubs(:now).returns(time)
199
+
200
+ @cache.write('foo', 'bar', :expires_in => 1.minute)
201
+ assert_equal 'bar', @cache.read('foo')
202
+
203
+ Time.stubs(:now).returns(time + 30)
204
+ assert_equal 'bar', @cache.read('foo')
205
+
206
+ Time.stubs(:now).returns(time + 61)
207
+ assert_nil @cache.read('foo')
208
+ end
209
+
210
+ def test_expires_in_as_float
211
+ time = Time.local(2012, 02, 03)
212
+ Time.stubs(:now).returns(time)
213
+
214
+ @cache.write('foo', 'bar', :expires_in => 60.0)
215
+ assert_equal 'bar', @cache.read('foo')
216
+
217
+ Time.stubs(:now).returns(time + 30)
218
+ assert_equal 'bar', @cache.read('foo')
219
+
220
+ Time.stubs(:now).returns(time + 61)
221
+ assert_nil @cache.read('foo')
222
+ end
223
+
224
+ def test_race_condition_protection
225
+ time = Time.now
226
+ @cache.write('foo', 'bar', :expires_in => 60)
227
+ Time.stubs(:now).returns(time + 61)
228
+ result = @cache.fetch('foo', :race_condition_ttl => 10) do
229
+ assert_equal 'bar', @cache.read('foo')
230
+ "baz"
231
+ end
232
+ assert_equal "baz", result
233
+ end
234
+
235
+ def test_race_condition_protection_is_limited
236
+ time = Time.now
237
+ @cache.write('foo', 'bar', :expires_in => 60)
238
+ Time.stubs(:now).returns(time + 71)
239
+ result = @cache.fetch('foo', :race_condition_ttl => 10) do
240
+ assert_equal nil, @cache.read('foo')
241
+ "baz"
242
+ end
243
+ assert_equal "baz", result
244
+ end
245
+
246
+ def test_race_condition_protection_is_safe
247
+ time = Time.now
248
+ @cache.write('foo', 'bar', :expires_in => 60)
249
+ Time.stubs(:now).returns(time + 61)
250
+ begin
251
+ @cache.fetch('foo', :race_condition_ttl => 10) do
252
+ assert_equal 'bar', @cache.read('foo')
253
+ raise ArgumentError.new
254
+ end
255
+ rescue ArgumentError
256
+ end
257
+ assert_equal "bar", @cache.read('foo')
258
+ Time.stubs(:now).returns(time + 71)
259
+ assert_nil @cache.read('foo')
260
+ end
261
+
262
+ def test_crazy_key_characters
263
+ crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-"
264
+ assert_equal true, @cache.write(crazy_key, "1", :raw => true)
265
+ assert_equal "1", @cache.read(crazy_key)
266
+ assert_equal "1", @cache.fetch(crazy_key)
267
+ assert_equal true, @cache.delete(crazy_key)
268
+ assert_equal "2", @cache.fetch(crazy_key, :raw => true) { "2" }
269
+ assert_equal 3, @cache.increment(crazy_key)
270
+ assert_equal 2, @cache.decrement(crazy_key)
271
+ end
272
+
273
+ # def test_really_long_keys
274
+ # key = ""
275
+ # 900.times{key << "x"}
276
+ # assert @cache.write(key, "bar")
277
+ # assert_equal "bar", @cache.read(key)
278
+ # assert_equal "bar", @cache.fetch(key)
279
+ # assert_nil @cache.read("#{key}x")
280
+ # assert_equal({key => "bar"}, @cache.read_multi(key))
281
+ # assert @cache.delete(key)
282
+ # end
283
+ end
284
+
285
+ module CacheIncrementDecrementBehavior
286
+ def test_increment
287
+ @cache.write('foo', 1, :raw => true)
288
+ assert_equal 1, @cache.read('foo').to_i
289
+ assert_equal 2, @cache.increment('foo')
290
+ assert_equal 2, @cache.read('foo').to_i
291
+ assert_equal 3, @cache.increment('foo')
292
+ assert_equal 3, @cache.read('foo').to_i
293
+ end
294
+
295
+ def test_decrement
296
+ @cache.write('foo', 3, :raw => true)
297
+ assert_equal 3, @cache.read('foo').to_i
298
+ assert_equal 2, @cache.decrement('foo')
299
+ assert_equal 2, @cache.read('foo').to_i
300
+ assert_equal 1, @cache.decrement('foo')
301
+ assert_equal 1, @cache.read('foo').to_i
302
+ end
303
+ end
304
+
305
+ class LibmemcachedStoreTest < MiniTest::Unit::TestCase
306
+ include CacheStoreBehavior
307
+ include CacheIncrementDecrementBehavior
308
+
309
+ def setup
310
+ @cache = ActiveSupport::Cache.lookup_store(:libmemcached_store, :expires_in => 60)
311
+ @cache.clear
312
+ @cache.silence!
313
+ end
314
+
315
+ def test_should_identify_cache_store
316
+ assert_kind_of ActiveSupport::Cache::LibmemcachedStore, @cache
317
+ end
318
+
319
+ def test_should_set_server_addresses_to_nil_if_none_are_given
320
+ assert_equal [], @cache.addresses
321
+ end
322
+
323
+ def test_should_set_custom_server_addresses
324
+ store = ActiveSupport::Cache.lookup_store :libmemcached_store, 'localhost', '192.168.1.1'
325
+ assert_equal %w(localhost 192.168.1.1), store.addresses
326
+ end
327
+
328
+ def test_should_enable_consistent_ketema_hashing_by_default
329
+ assert_equal :consistent_ketama, @cache.options[:distribution]
330
+ end
331
+
332
+ def test_should_not_enable_non_blocking_io_by_default
333
+ assert_equal false, @cache.options[:no_block]
334
+ end
335
+
336
+ def test_should_not_enable_server_failover_by_default
337
+ assert_nil @cache.options[:failover]
338
+ end
339
+
340
+ def test_should_allow_configuration_of_custom_options
341
+ options = {
342
+ :tcp_nodelay => true,
343
+ :distribution => :modula
344
+ }
345
+
346
+ store = ActiveSupport::Cache.lookup_store :libmemcached_store, 'localhost', options
347
+
348
+ assert_equal :modula, store.options[:distribution]
349
+ assert_equal true, store.options[:tcp_nodelay]
350
+ end
351
+ end
@@ -0,0 +1,10 @@
1
+ module SessionAutoloadTest
2
+ class Foo
3
+ def initialize(bar='baz')
4
+ @bar = bar
5
+ end
6
+ def inspect
7
+ "#<#{self.class} bar:#{@bar.inspect}>"
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,5 @@
1
+ ENV["RAILS_ENV"] = "test"
2
+ $:.unshift File.expand_path('../../lib', __FILE__)
3
+
4
+ require 'minitest/autorun'
5
+ require 'mocha'
metadata ADDED
@@ -0,0 +1,170 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rails3_libmemcached_store
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Christopher Cocchi-Perrier
9
+ - Ben Hutton
10
+ - Jeffrey Hardy
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2012-08-16 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: memcached
18
+ requirement: !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ! '>='
22
+ - !ruby/object:Gem::Version
23
+ version: '0'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ! '>='
30
+ - !ruby/object:Gem::Version
31
+ version: '0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rack
34
+ requirement: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :development
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ - !ruby/object:Gem::Dependency
49
+ name: rake
50
+ requirement: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ! '>='
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ - !ruby/object:Gem::Dependency
65
+ name: mocha
66
+ requirement: !ruby/object:Gem::Requirement
67
+ none: false
68
+ requirements:
69
+ - - ! '>='
70
+ - !ruby/object:Gem::Version
71
+ version: '0'
72
+ type: :development
73
+ prerelease: false
74
+ version_requirements: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ - !ruby/object:Gem::Dependency
81
+ name: activesupport
82
+ requirement: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '3'
88
+ type: :development
89
+ prerelease: false
90
+ version_requirements: !ruby/object:Gem::Requirement
91
+ none: false
92
+ requirements:
93
+ - - ! '>='
94
+ - !ruby/object:Gem::Version
95
+ version: '3'
96
+ - !ruby/object:Gem::Dependency
97
+ name: actionpack
98
+ requirement: !ruby/object:Gem::Requirement
99
+ none: false
100
+ requirements:
101
+ - - ! '>='
102
+ - !ruby/object:Gem::Version
103
+ version: '3'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ! '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '3'
112
+ description: ! "An ActiveSupport cache store that uses the C-based libmemcached client
113
+ through\n Evan Weaver's Ruby/SWIG wrapper, memcached. libmemcached is fast,
114
+ lightweight,\n and supports consistent hashing, non-blocking IO, and graceful
115
+ server failover."
116
+ email: cocchi.c@gmail.com
117
+ executables: []
118
+ extensions: []
119
+ extra_rdoc_files: []
120
+ files:
121
+ - .gitignore
122
+ - .travis.yml
123
+ - Gemfile
124
+ - MIT-LICENSE
125
+ - README.md
126
+ - Rakefile
127
+ - gemfiles/rails30.gemfile
128
+ - gemfiles/rails31.gemfile
129
+ - gemfiles/rails32.gemfile
130
+ - lib/action_dispatch/session/libmemcached_store.rb
131
+ - lib/active_support/cache/compressed_libmemcached_store.rb
132
+ - lib/active_support/cache/libmemcached_store.rb
133
+ - lib/libmemcached_store.rb
134
+ - lib/version.rb
135
+ - libmemcached_store.gemspec
136
+ - test/action_dispatch/abstract_unit.rb
137
+ - test/action_dispatch/libmemcached_store_test.rb
138
+ - test/active_support/libmemcached_store_test.rb
139
+ - test/fixtures/session_autoload_test.rb
140
+ - test/test_helper.rb
141
+ homepage: http://github.com/ccocchi/libmemcached_store
142
+ licenses: []
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ none: false
149
+ requirements:
150
+ - - ! '>='
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ required_rubygems_version: !ruby/object:Gem::Requirement
154
+ none: false
155
+ requirements:
156
+ - - ! '>='
157
+ - !ruby/object:Gem::Version
158
+ version: '0'
159
+ requirements: []
160
+ rubyforge_project:
161
+ rubygems_version: 1.8.23
162
+ signing_key:
163
+ specification_version: 3
164
+ summary: ActiveSupport 3+ cache store for the C-based libmemcached client
165
+ test_files:
166
+ - test/action_dispatch/abstract_unit.rb
167
+ - test/action_dispatch/libmemcached_store_test.rb
168
+ - test/active_support/libmemcached_store_test.rb
169
+ - test/fixtures/session_autoload_test.rb
170
+ - test/test_helper.rb