rails3_libmemcached_store 0.4.0

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