rails3_libmemcached_store 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
data/BENCHMARKS ADDED
@@ -0,0 +1,37 @@
1
+ Testing with
2
+ ruby 1.9.3p125 (2012-02-16 revision 34643) [x86_64-darwin10.8.0]
3
+ Dalli 2.1.0
4
+ Libmemcached_store 0.5.0
5
+ user system total real
6
+ write:short:dalli 0.260000 0.060000 0.320000 ( 0.341703)
7
+ write:short:libm 0.040000 0.030000 0.070000 ( 0.176990)
8
+ write:long:dalli 0.240000 0.060000 0.300000 ( 0.327882)
9
+ write:long:libm 0.040000 0.030000 0.070000 ( 0.213028)
10
+ write:raw:dalli 0.210000 0.060000 0.270000 ( 0.271676)
11
+ write:raw:libm 0.040000 0.030000 0.070000 ( 0.149668)
12
+
13
+ read:miss:dalli 0.310000 0.080000 0.390000 ( 0.395997)
14
+ read:miss:libm 0.060000 0.040000 0.100000 ( 0.199834)
15
+ read:miss2:dalli 0.210000 0.060000 0.270000 ( 0.274769)
16
+ read:miss2:libm 0.060000 0.040000 0.100000 ( 0.219939)
17
+ read:exist:dalli 0.210000 0.060000 0.270000 ( 0.330582)
18
+ read:exist:libm 0.050000 0.050000 0.100000 ( 0.186952)
19
+ read:expired:dalli 0.240000 0.060000 0.300000 ( 0.306864)
20
+ read:expired:libm 0.060000 0.040000 0.100000 ( 0.212305)
21
+ read:raw:dalli 0.320000 0.070000 0.390000 ( 0.391410)
22
+ read:raw:libm 0.040000 0.050000 0.090000 ( 0.175050)
23
+
24
+ exist:miss:dalli 0.200000 0.060000 0.260000 ( 0.271294)
25
+ exist:miss:libm 0.060000 0.040000 0.100000 ( 0.211563)
26
+ exist:hit:dalli 0.200000 0.060000 0.260000 ( 0.324383)
27
+ exist:hit:libm 0.050000 0.050000 0.100000 ( 0.173287)
28
+
29
+ delete:miss:dalli 0.230000 0.050000 0.280000 ( 0.281045)
30
+ delete:miss:libm 0.050000 0.030000 0.080000 ( 0.213801)
31
+ delete:hit:dalli 0.290000 0.060000 0.350000 ( 0.354938)
32
+ delete:hit:libm 0.060000 0.030000 0.090000 ( 0.212315)
33
+
34
+ increment:dalli 0.210000 0.050000 0.260000 ( 0.271489)
35
+ increment:libm 0.040000 0.040000 0.080000 ( 0.153906)
36
+ decrement:dalli 0.220000 0.050000 0.270000 ( 0.292662)
37
+ decrement:libm 0.040000 0.040000 0.080000 ( 0.147424)
data/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.5.0
4
+ * Use Memcached#exist if available (performance improvement ~25%)
5
+ * Correctly escape bad characters and too long keys
6
+ * Add benchmarks
7
+ * Remove the use of ActiveSupport::Entry which was a performance bottleneck #3
8
+
9
+ ## 0.4.0
10
+ * Optimize read_multi to only make one call to memecached server
11
+ * Update test suite to reflect Rails' one
12
+ * Add session store tests
data/Gemfile CHANGED
@@ -1,3 +1,7 @@
1
1
  source "http://rubygems.org"
2
2
 
3
- gemspec
3
+ gemspec
4
+
5
+ group :test do
6
+ gem 'dalli'
7
+ end
data/README.md CHANGED
@@ -8,11 +8,11 @@ This cache is designed for Rails 3+ applications.
8
8
 
9
9
  You'll need the memcached gem installed:
10
10
 
11
- ```ruby
11
+ ```ruby
12
12
  gem install memcached
13
13
  ```
14
14
 
15
- or in your Gemfile
15
+ or in your Gemfile
16
16
 
17
17
  ```ruby
18
18
  gem 'memcached'
@@ -48,18 +48,30 @@ designation. If no port is given, 11211 is assumed:
48
48
  config.cache_store = :libmemcached_store, %w(cache-01 cache-02 127.0.0.1:11212)
49
49
  ```
50
50
 
51
- Other options are passed directly to the memcached client
52
-
51
+ Standard Rails cache store options can be used
52
+
53
+ ```ruby
54
+ config.cache_store = :libmemcached_store, '127.0.0.1:11211', :compress => true, :expires_in => 3600
55
+ ```
56
+
57
+ More advanced options can be passed directly to the client
58
+
53
59
  ```ruby
54
- config.cache_store = :libmemcached_store, '127.0.0.1:11211', :default_ttl => 3600, :compress => true
60
+ config.cache_store = :libmemcached_store, '127.0.0.1:11211', :client => { :binary_protocol => true, :no_block => true }
55
61
  ```
56
62
 
57
63
  You can also use `:libmemcached_store` to store your application sessions
58
64
 
59
65
  ```ruby
66
+ require 'action_dispatch/session/libmemcached_store'
60
67
  config.session_store = :libmemcached_store, :namespace => '_session', :expire_after => 1800
61
68
  ```
62
69
 
70
+ ## Performance
71
+
72
+ Used with Rails, __libmemcached_store__ is at least 1.5x faster than __dalli__. See [BENCHMARKS](https://github.com/ccocchi/libmemcached_store/blob/master/BENCHMARKS)
73
+ for details
74
+
63
75
  ## Props
64
76
 
65
77
  Thanks to Brian Aker ([http://tangent.org](http://tangent.org)) for creating libmemcached, and Evan
@@ -1,17 +1,17 @@
1
1
  require 'memcached'
2
- require 'rack/session/abstract/id'
2
+ require 'action_dispatch/middleware/session/abstract_store'
3
3
 
4
4
  module ActionDispatch
5
5
  module Session
6
6
  class LibmemcachedStore < AbstractStore
7
7
 
8
- DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS.merge(:prefix_key => 'rack:session', :memcache_server => 'localhost:11211')
9
-
10
8
  def initialize(app, options = {})
11
9
  options[:expire_after] ||= options[:expires]
12
10
  super
11
+ client_options = { default_ttl: options.fetch(:expire_after, 0) }
12
+ client_options[:namespace] = options[:namespace] || 'rack:session'
13
13
  @mutex = Mutex.new
14
- @pool = options[:cache] || Memcached.new(@default_options[:memcache_server], @default_options)
14
+ @pool = options[:cache] || Memcached.new(@default_options[:memcache_server], client_options)
15
15
  end
16
16
 
17
17
  private
@@ -20,7 +20,7 @@ module ActionDispatch
20
20
  loop do
21
21
  sid = super
22
22
  begin
23
- @pool.get(sid)
23
+ @pool.exist(sid)
24
24
  rescue Memcached::NotFound
25
25
  break sid
26
26
  end
@@ -40,8 +40,7 @@ module ActionDispatch
40
40
  end
41
41
 
42
42
  def set_session(env, session_id, new_session, options = {})
43
- expiry = options[:expire_after]
44
- expiry = expiry.nil? ? 0 : expiry + 1
43
+ expiry = options[:expire_after].to_i
45
44
 
46
45
  with_lock(env, false) do
47
46
  @pool.set(session_id, new_session, expiry)
@@ -64,7 +63,7 @@ module ActionDispatch
64
63
  with_lock(env, false) do
65
64
  @pool.delete(sid)
66
65
  end
67
- end
66
+ end
68
67
  end
69
68
 
70
69
  def with_lock(env, default)
@@ -1,67 +1,169 @@
1
1
  require 'memcached'
2
+ require 'memcached/get_with_flags'
2
3
 
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
4
+ require 'digest/md5'
14
5
 
15
6
  module ActiveSupport
16
7
  module Cache
17
- class LibmemcachedStore < Store
8
+
9
+ #
10
+ # Store using memcached gem as client
11
+ #
12
+ # Global options can be passed to be applied to each method by default.
13
+ # Supported options are
14
+ # * <tt>:compress</tt> : if set to true, data will be compress before stored
15
+ # * <tt>:compress_threshold</tt> : specify the threshold at which to compress
16
+ # value, default is 4K
17
+ # * <tt>:namespace</tt> : prepend each key with this value for simple namespacing
18
+ # * <tt>:expires_in</tt> : default TTL in seconds for each. Default value is 0, i.e. forever
19
+ # Specific value can be passed per key with write and fetch command.
20
+ #
21
+ # Options can also be passed direclty to the memcache client, via the <tt>:client</tt>
22
+ # option. For example, if you want to use pipelining, you can use
23
+ # :client => { :no_block => true }
24
+ #
25
+ class LibmemcachedStore
18
26
  attr_reader :addresses
19
27
 
20
- DEFAULT_OPTIONS = {
21
- :distribution => :consistent_ketama,
22
- :binary_protocol => true
23
- }
28
+ DEFAULT_CLIENT_OPTIONS = { distribution: :consistent_ketama, binary_protocol: true, default_ttl: 0 }
29
+ ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
30
+ DEFAULT_COMPRESS_THRESHOLD = 4096
31
+ FLAG_COMPRESSED = 0x2
32
+
33
+ attr_reader :silence, :options
34
+ alias_method :silence?, :silence
35
+
36
+ # Silence the logger.
37
+ def silence!
38
+ @silence = true
39
+ self
40
+ end
41
+
42
+ # Silence the logger within a block.
43
+ def mute
44
+ previous_silence, @silence = defined?(@silence) && @silence, true
45
+ yield
46
+ ensure
47
+ @silence = previous_silence
48
+ end
24
49
 
25
50
  def initialize(*addresses)
26
51
  addresses.flatten!
27
- @options = addresses.extract_options!
52
+ options = addresses.extract_options!
53
+ client_options = options.delete(:client) || {}
54
+ if options[:namespace]
55
+ client_options[:prefix_key] = options.delete(:namespace)
56
+ client_options[:prefix_delimiter] = ':'
57
+ @namespace_length = client_options[:prefix_key].length + 1
58
+ else
59
+ @namespace_length = 0
60
+ end
61
+ client_options[:default_ttl] = options.delete(:expires_in).to_i if options[:expires_in]
62
+
63
+ @options = options.reverse_merge(compress_threshold: DEFAULT_COMPRESS_THRESHOLD)
28
64
  @addresses = addresses
29
- @cache = Memcached.new(@addresses, @options.reverse_merge(DEFAULT_OPTIONS))
65
+ @cache = Memcached.new(@addresses, client_options.reverse_merge(DEFAULT_CLIENT_OPTIONS))
66
+ @cache.instance_eval { send(:extend, GetWithFlags) }
67
+ end
68
+
69
+ def fetch(key, options = nil)
70
+ if block_given?
71
+ key = expanded_key(key)
72
+ unless options && options[:force]
73
+ entry = instrument(:read, key, options) do |payload|
74
+ payload[:super_operation] = :fetch if payload
75
+ read_entry(key, options)
76
+ end
77
+ end
78
+
79
+ if entry.nil?
80
+ result = instrument(:generate, key, options) do |payload|
81
+ yield
82
+ end
83
+ write_entry(key, result, options)
84
+ result
85
+ else
86
+ instrument(:fetch_hit, key, options) { |payload| }
87
+ entry
88
+ end
89
+ else
90
+ read(key, options)
91
+ end
92
+ end
93
+
94
+ def read(key, options = nil)
95
+ key = expanded_key(key)
96
+ instrument(:read, key, options) do |payload|
97
+ entry = read_entry(key, options)
98
+ payload[:hit] = !!entry if payload
99
+ entry
100
+ end
101
+ end
102
+
103
+ def write(key, value, options = nil)
104
+ key = expanded_key(key)
105
+ instrument(:write, key, options) do |payload|
106
+ write_entry(key, value, options)
107
+ end
108
+ end
109
+
110
+ def delete(key, options = nil)
111
+ key = expanded_key(key)
112
+ instrument(:delete, key) do |payload|
113
+ delete_entry(key, options)
114
+ end
115
+ end
116
+
117
+ def exist?(key, options = nil)
118
+ key = expanded_key(key)
119
+ instrument(:exist?, key) do |payload|
120
+ if @cache.respond_to?(:exist)
121
+ @cache.exist(escape_and_normalize(key))
122
+ true
123
+ else
124
+ read_entry(key, options) != nil
125
+ end
126
+ end
127
+ rescue Memcached::NotFound
128
+ false
30
129
  end
31
130
 
32
131
  def increment(key, amount = 1, options = nil)
132
+ key = expanded_key(key)
33
133
  instrument(:increment, key, amount: amount) do
34
- @cache.incr(key, amount)
134
+ @cache.incr(escape_and_normalize(key), amount)
35
135
  end
36
- rescue Memcached::Error
136
+ rescue Memcached::NotFound
137
+ nil
138
+ rescue Memcached::Error => e
139
+ log_error(e)
37
140
  nil
38
141
  end
39
142
 
40
143
  def decrement(key, amount = 1, options = nil)
144
+ key = expanded_key(key)
41
145
  instrument(:decrement, key, amount: amount) do
42
- @cache.decr(key, amount)
146
+ @cache.decr(escape_and_normalize(key), amount)
43
147
  end
44
- rescue Memcached::Error
148
+ rescue Memcached::NotFound
149
+ nil
150
+ rescue Memcached::Error => e
151
+ log_error(e)
45
152
  nil
46
153
  end
47
154
 
48
- #
49
- # Optimize read_multi to only make one call to memcached
50
- # server.
51
- #
52
155
  def read_multi(*names)
156
+ names.flatten!
53
157
  options = names.extract_options!
54
- options = merged_options(options)
55
158
 
56
159
  return {} if names.empty?
57
160
 
58
- keys_to_names = Hash[names.map {|name| [namespaced_key(name, options), name] }]
59
- raw_values = @cache.get(keys_to_names.keys, false)
161
+ mapping = Hash[names.map {|name| [escape_and_normalize(expanded_key(name)), name] }]
162
+ raw_values, flags = @cache.get(mapping.keys, false, true)
60
163
 
61
164
  values = {}
62
165
  raw_values.each do |key, value|
63
- entry = deserialize_entry(value)
64
- values[keys_to_names[key]] = entry.value unless entry.expired?
166
+ values[mapping[key]] = deserialize(value, options[:raw], flags[key])
65
167
  end
66
168
  values
67
169
  end
@@ -77,7 +179,9 @@ module ActiveSupport
77
179
  protected
78
180
 
79
181
  def read_entry(key, options = nil)
80
- deserialize_entry(@cache.get(key, false))
182
+ options ||= {}
183
+ raw_value, flags = @cache.get(escape_and_normalize(key), false, true)
184
+ deserialize(raw_value, options[:raw], flags)
81
185
  rescue Memcached::NotFound
82
186
  nil
83
187
  rescue Memcached::Error => e
@@ -85,13 +189,18 @@ module ActiveSupport
85
189
  nil
86
190
  end
87
191
 
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
192
  def write_entry(key, entry, options = nil)
91
- method = (options && options[:unless_exist]) ? :add : :set
92
- value = options[:raw] ? entry.value.to_s : entry
193
+ options = options ? @options.merge(options) : @options
194
+ method = options[:unless_exist] ? :add : :set
195
+ entry = options[:raw] ? entry.to_s : Marshal.dump(entry)
196
+ flags = 0
93
197
 
94
- @cache.send(method, key, value, expires_in(options), marshal?(options))
198
+ if options[:compress] && entry.bytesize >= options[:compress_threshold]
199
+ entry = Zlib::Deflate.deflate(entry)
200
+ flags |= FLAG_COMPRESSED
201
+ end
202
+
203
+ @cache.send(method, escape_and_normalize(key), entry, options[:expires_in].to_i, false, flags)
95
204
  true
96
205
  rescue Memcached::Error => e
97
206
  log_error(e)
@@ -99,7 +208,7 @@ module ActiveSupport
99
208
  end
100
209
 
101
210
  def delete_entry(key, options = nil)
102
- @cache.delete(key)
211
+ @cache.delete(escape_and_normalize(key))
103
212
  true
104
213
  rescue Memcached::NotFound
105
214
  false
@@ -109,27 +218,66 @@ module ActiveSupport
109
218
  end
110
219
 
111
220
  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
221
+
222
+ def deserialize(value, raw = false, flags = 0)
223
+ value = Zlib::Inflate.inflate(value) if (flags & FLAG_COMPRESSED) != 0
224
+ raw ? value : Marshal.load(value)
225
+ rescue TypeError, ArgumentError
226
+ value
227
+ end
228
+
229
+ def escape_and_normalize(key)
230
+ key = key.to_s.force_encoding("BINARY").gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
231
+ key_length = key.length
232
+
233
+ return key if @namespace_length + key_length <= 250
234
+
235
+ max_key_length = 213 - @namespace_length
236
+ "#{key[0, max_key_length]}:md5:#{Digest::MD5.hexdigest(key)}"
237
+ end
238
+
239
+ def expanded_key(key) # :nodoc:
240
+ return key.cache_key.to_s if key.respond_to?(:cache_key)
241
+
242
+ case key
243
+ when Array
244
+ if key.size > 1
245
+ key = key.collect { |element| expanded_key(element) }
246
+ else
247
+ key = key.first
248
+ end
249
+ when Hash
250
+ key = key.sort_by { |k,_| k.to_s }.collect { |k, v| "#{k}=#{v}" }
118
251
  end
252
+
253
+ key.to_param
119
254
  end
120
255
 
121
- def expires_in(options)
122
- (options || {})[:expires_in].to_i
256
+ def instrument(operation, key, options=nil)
257
+ log(operation, key, options)
258
+
259
+ if ActiveSupport::Cache::Store.instrument
260
+ payload = { :key => key }
261
+ payload.merge!(options) if options.is_a?(Hash)
262
+ ActiveSupport::Notifications.instrument("cache_#{operation}.active_support", payload){ yield(payload) }
263
+ else
264
+ yield(nil)
265
+ end
123
266
  end
124
267
 
125
- def marshal?(options)
126
- !(options || {})[:raw]
268
+ def log(operation, key, options=nil)
269
+ return unless !silence? && logger && logger.debug?
270
+ logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}")
127
271
  end
128
272
 
129
273
  def log_error(exception)
130
- return unless logger && logger.error?
274
+ return unless !silence? && logger && logger.error?
131
275
  logger.error "MemcachedError (#{exception.inspect}): #{exception.message}"
132
276
  end
277
+
278
+ def logger
279
+ Rails.logger
280
+ end
133
281
  end
134
282
  end
135
283
  end
@@ -1,3 +1,2 @@
1
1
  require 'active_support/cache/libmemcached_store'
2
- require 'active_support/cache/compressed_libmemcached_store'
3
2
  require 'action_dispatch/session/libmemcached_store'
@@ -0,0 +1,42 @@
1
+ #
2
+ # Allow get method to returns value + entry's flags
3
+ # This is useful to set compression flag.
4
+ #
5
+ module GetWithFlags
6
+ def get(keys, marshal=true, with_flags=false)
7
+ if keys.is_a? Array
8
+ # Multi get
9
+ ret = Memcached::Lib.memcached_mget(@struct, keys);
10
+ check_return_code(ret, keys)
11
+
12
+ hash, flags_hash = {}, {}
13
+ value, key, flags, ret = Memcached::Lib.memcached_fetch_rvalue(@struct)
14
+ while ret != 21 do # Lib::MEMCACHED_END
15
+ if ret == 0 # Lib::MEMCACHED_SUCCESS
16
+ hash[key] = value
17
+ flags_hash[key] = flags if with_flags
18
+ elsif ret != 16 # Lib::MEMCACHED_NOTFOUND
19
+ check_return_code(ret, key)
20
+ end
21
+ value, key, flags, ret = Memcached::Lib.memcached_fetch_rvalue(@struct)
22
+ end
23
+ if marshal
24
+ hash.each do |key, value|
25
+ hash[key] = Marshal.load(value)
26
+ end
27
+ end
28
+ with_flags ? [hash, flags_hash] : hash
29
+ else
30
+ # Single get
31
+ value, flags, ret = Memcached::Lib.memcached_get_rvalue(@struct, keys)
32
+ check_return_code(ret, keys)
33
+ value = Marshal.load(value) if marshal
34
+ with_flags ? [value, flags] : value
35
+ end
36
+ rescue => e
37
+ tries ||= 0
38
+ raise unless tries < options[:exception_retry_limit] && should_retry(e)
39
+ tries += 1
40
+ retry
41
+ end
42
+ end
data/lib/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module LibmemcachedStore
2
- VERSION = "0.4.0".freeze
2
+ VERSION = "0.5.0".freeze
3
3
  end
@@ -1,3 +1,5 @@
1
+ # encoding: utf-8
2
+
1
3
  require 'test_helper'
2
4
  require 'memcached'
3
5
  require 'active_support'
@@ -7,67 +9,101 @@ require 'active_support/cache/libmemcached_store'
7
9
 
8
10
  # Make it easier to get at the underlying cache options during testing.
9
11
  class ActiveSupport::Cache::LibmemcachedStore
10
- delegate :options, :to => '@cache'
12
+ def client_options
13
+ @cache.options
14
+ end
11
15
  end
12
16
 
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')
17
+ class MockUser
18
+ def cache_key
19
+ 'foo'
23
20
  end
21
+ end
24
22
 
23
+ module CacheStoreBehavior
25
24
  def test_fetch_without_cache_miss
26
25
  @cache.write('foo', 'bar')
27
- @cache.expects(:write).never
26
+ @cache.expects(:write_entry).never
28
27
  assert_equal 'bar', @cache.fetch('foo') { 'baz' }
29
28
  end
30
29
 
31
30
  def test_fetch_with_cache_miss
32
- @cache.expects(:write).with('foo', 'baz', @cache.options)
31
+ @cache.expects(:write_entry).with('foo', 'baz', nil)
33
32
  assert_equal 'baz', @cache.fetch('foo') { 'baz' }
34
33
  end
35
34
 
36
35
  def test_fetch_with_forced_cache_miss
37
36
  @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' }
37
+ @cache.expects(:read_entry).never
38
+ @cache.expects(:write_entry).with('foo', 'baz', force: true)
39
+ assert_equal 'baz', @cache.fetch('foo', force: true) { 'baz' }
40
+ end
41
+
42
+ def test_fetch_with_cached_false
43
+ @cache.write('foo', false)
44
+ refute @cache.fetch('foo') { raise }
45
+ end
46
+
47
+ def test_fetch_with_raw_object
48
+ o = Object.new
49
+ o.instance_variable_set :@foo, 'bar'
50
+ assert_equal o, @cache.fetch('foo', raw: true) { o }
41
51
  end
42
52
 
43
- def test_fetch_with_cached_nil
44
- @cache.write('foo', nil)
45
- @cache.expects(:write).never
46
- assert_nil @cache.fetch('foo') { 'baz' }
53
+ def test_fetch_with_cache_key
54
+ u = MockUser.new
55
+ @cache.write(u.cache_key, 'bar')
56
+ assert_equal 'bar', @cache.fetch(u) { raise }
57
+ end
58
+
59
+ def test_should_read_and_write_strings
60
+ assert @cache.write('foo', 'bar')
61
+ assert_equal 'bar', @cache.read('foo')
47
62
  end
48
63
 
49
64
  def test_should_read_and_write_hash
50
- assert_equal true, @cache.write('foo', {:a => "b"})
51
- assert_equal({:a => "b"}, @cache.read('foo'))
65
+ assert @cache.write('foo', { a: 'b' })
66
+ assert_equal({ a: 'b' }, @cache.read('foo'))
52
67
  end
53
68
 
54
69
  def test_should_read_and_write_integer
55
- assert_equal true, @cache.write('foo', 1)
70
+ assert @cache.write('foo', 1)
56
71
  assert_equal 1, @cache.read('foo')
57
72
  end
58
73
 
59
74
  def test_should_read_and_write_nil
60
- assert_equal true, @cache.write('foo', nil)
75
+ assert @cache.write('foo', nil)
61
76
  assert_equal nil, @cache.read('foo')
62
77
  end
63
78
 
64
79
  def test_should_read_and_write_false
65
80
  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
81
+ assert_equal false, @cache.read('foo')
82
+ end
83
+
84
+ def test_read_and_write_compressed_data
85
+ @cache.write('foo', 'bar', :compress => true, :compress_threshold => 1)
86
+ assert_equal 'bar', @cache.read('foo')
87
+ end
88
+
89
+ def test_write_should_overwrite
90
+ @cache.write('foo', 'bar')
91
+ @cache.write('foo', 'baz')
92
+ assert_equal 'baz', @cache.read('foo')
93
+ end
94
+
95
+ def test_write_compressed_data
96
+ @cache.write('foo', 'bar', :compress => true, :compress_threshold => 1, :raw => true)
97
+ assert_equal Zlib::Deflate.deflate('bar'), @cache.instance_variable_get(:@cache).get('foo', false)
98
+ end
99
+
100
+ def test_read_miss
101
+ assert_nil @cache.read('foo')
102
+ end
103
+
104
+ def test_read_should_return_a_different_object_id_each_time_it_is_called
105
+ @cache.write('foo', 'bar')
106
+ refute_equal @cache.read('foo').object_id, @cache.read('foo').object_id
71
107
  end
72
108
 
73
109
  def test_read_multi
@@ -77,94 +113,75 @@ module CacheStoreBehavior
77
113
  assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi('foo', 'fu'))
78
114
  end
79
115
 
80
- def test_read_multi_with_expires
81
- @cache.write('foo', 'bar', :expires_in => 0.001)
116
+ def test_read_multi_with_array
117
+ @cache.write('foo', 'bar')
82
118
  @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
119
+ assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi(['foo', 'fu']))
94
120
  end
95
121
 
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))
122
+ def test_read_multi_with_raw
123
+ @cache.write('foo', 'bar', :raw => true)
124
+ @cache.write('fu', 'baz', :raw => true)
125
+ assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi('foo', 'fu'))
101
126
  end
102
127
 
103
- def test_read_and_write_compressed_nil
104
- @cache.write('foo', nil, :compress => true)
105
- assert_nil @cache.read('foo')
128
+ def test_read_multi_with_compress
129
+ @cache.write('foo', 'bar', :compress => true, :compress_threshold => 1)
130
+ @cache.write('fu', 'baz', :compress => true, :compress_threshold => 1)
131
+ assert_equal({"foo" => "bar", "fu" => "baz"}, @cache.read_multi('foo', 'fu'))
106
132
  end
107
133
 
108
134
  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")
135
+ o = MockUser.new
136
+ @cache.write(o, 'bar')
137
+ assert_equal 'bar', @cache.read('foo')
115
138
  end
116
139
 
117
140
  def test_param_as_cache_key
118
141
  obj = Object.new
119
142
  def obj.to_param
120
- "foo"
143
+ 'foo'
121
144
  end
122
- @cache.write(obj, "bar")
123
- assert_equal "bar", @cache.read("foo")
145
+ @cache.write(obj, 'bar')
146
+ assert_equal 'bar', @cache.read('foo')
124
147
  end
125
148
 
126
149
  def test_array_as_cache_key
127
- @cache.write([:fu, "foo"], "bar")
128
- assert_equal "bar", @cache.read("fu/foo")
150
+ @cache.write([:fu, 'foo'], 'bar')
151
+ assert_equal 'bar', @cache.read('fu/foo')
129
152
  end
130
153
 
131
154
  def test_hash_as_cache_key
132
- @cache.write({:foo => 1, :fu => 2}, "bar")
133
- assert_equal "bar", @cache.read("foo=1/fu=2")
155
+ @cache.write({:foo => 1, :fu => 2}, 'bar')
156
+ assert_equal 'bar', @cache.read('foo=1/fu=2')
134
157
  end
135
158
 
136
159
  def test_keys_are_case_sensitive
137
- @cache.write("foo", "bar")
138
- assert_nil @cache.read("FOO")
160
+ @cache.write('foo', 'bar')
161
+ assert_nil @cache.read('FOO')
139
162
  end
140
163
 
141
- def test_exist
142
- @cache.write('foo', 'bar')
143
- assert_equal true, @cache.exist?('foo')
144
- assert_equal false, @cache.exist?('bar')
164
+ def test_keys_with_spaces
165
+ assert_equal 'baz', @cache.fetch('foo bar') { 'baz' }
145
166
  end
146
167
 
147
- def test_nil_exist
148
- @cache.write('foo', nil)
149
- assert_equal true, @cache.exist?('foo')
168
+ def test_exist
169
+ @cache.write('foo', 'bar')
170
+ assert @cache.exist?('foo')
171
+ refute @cache.exist?('bar')
150
172
  end
151
173
 
152
174
  def test_delete
153
175
  @cache.write('foo', 'bar')
154
176
  assert @cache.exist?('foo')
155
- assert_equal true, @cache.delete('foo')
156
- assert !@cache.exist?('foo')
177
+ assert @cache.delete('foo')
178
+ refute @cache.exist?('foo')
157
179
  end
158
180
 
159
181
  def test_delete_with_unexistent_key
160
182
  @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
183
+ refute @cache.exist?('foo')
184
+ refute @cache.delete('foo')
168
185
  end
169
186
 
170
187
  def test_store_objects_should_be_immutable
@@ -179,112 +196,42 @@ module CacheStoreBehavior
179
196
  assert_equal 'baz', bar.gsub!(/r/, 'z')
180
197
  end
181
198
 
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
199
  def test_crazy_key_characters
263
200
  crazy_key = "#/:*(<+=> )&$%@?;'\"\'`~-"
264
- assert_equal true, @cache.write(crazy_key, "1", :raw => true)
201
+ assert @cache.write(crazy_key, "1", :raw => true)
265
202
  assert_equal "1", @cache.read(crazy_key)
266
203
  assert_equal "1", @cache.fetch(crazy_key)
267
- assert_equal true, @cache.delete(crazy_key)
204
+ assert @cache.delete(crazy_key)
205
+ refute @cache.exist?(crazy_key)
268
206
  assert_equal "2", @cache.fetch(crazy_key, :raw => true) { "2" }
269
207
  assert_equal 3, @cache.increment(crazy_key)
270
208
  assert_equal 2, @cache.decrement(crazy_key)
271
209
  end
272
210
 
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
211
+ def test_really_long_keys
212
+ key = "a" * 251
213
+ assert @cache.write(key, "bar")
214
+ assert_equal "bar", @cache.read(key)
215
+ assert_equal "bar", @cache.fetch(key)
216
+ assert_nil @cache.read("#{key}x")
217
+ assert_equal({key => "bar"}, @cache.read_multi(key))
218
+ assert @cache.delete(key)
219
+ refute @cache.exist?(key)
220
+ assert @cache.write(key, '2', :raw => true)
221
+ assert_equal 3, @cache.increment(key)
222
+ assert_equal 2, @cache.decrement(key)
223
+ end
224
+
225
+ def test_really_long_keys_with_namespace
226
+ @cache = ActiveSupport::Cache.lookup_store(:libmemcached_store, :expires_in => 60, :namespace => 'namespace')
227
+ @cache.silence!
228
+ test_really_long_keys
229
+ end
283
230
  end
284
231
 
285
232
  module CacheIncrementDecrementBehavior
286
233
  def test_increment
287
- @cache.write('foo', 1, :raw => true)
234
+ @cache.write('foo', '1', :raw => true)
288
235
  assert_equal 1, @cache.read('foo').to_i
289
236
  assert_equal 2, @cache.increment('foo')
290
237
  assert_equal 2, @cache.read('foo').to_i
@@ -293,13 +240,36 @@ module CacheIncrementDecrementBehavior
293
240
  end
294
241
 
295
242
  def test_decrement
296
- @cache.write('foo', 3, :raw => true)
243
+ @cache.write('foo', '3', :raw => true)
297
244
  assert_equal 3, @cache.read('foo').to_i
298
245
  assert_equal 2, @cache.decrement('foo')
299
246
  assert_equal 2, @cache.read('foo').to_i
300
247
  assert_equal 1, @cache.decrement('foo')
301
248
  assert_equal 1, @cache.read('foo').to_i
302
249
  end
250
+
251
+ def test_increment_decrement_non_existing_keys
252
+ @cache.expects(:log_error).never
253
+ assert_nil @cache.increment('foo')
254
+ assert_nil @cache.decrement('bar')
255
+ end
256
+ end
257
+
258
+ module CacheCompressBehavior
259
+ def test_read_and_write_compressed_small_data
260
+ @cache.write('foo', 'bar', :compress => true)
261
+ raw_value = @cache.send(:read_entry, 'foo', {}).raw_value
262
+ assert_equal 'bar', @cache.read('foo')
263
+ value = Marshal.load(raw_value) rescue raw_value
264
+ assert_equal 'bar', value
265
+ end
266
+
267
+ def test_read_and_write_compressed_large_data
268
+ @cache.write('foo', 'bar', :compress => true, :compress_threshold => 2)
269
+ raw_value = @cache.send(:read_entry, 'foo', {}).raw_value
270
+ assert_equal 'bar', @cache.read('foo')
271
+ assert_equal 'bar', Marshal.load(Zlib::Inflate.inflate(raw_value))
272
+ end
303
273
  end
304
274
 
305
275
  class LibmemcachedStoreTest < MiniTest::Unit::TestCase
@@ -307,7 +277,7 @@ class LibmemcachedStoreTest < MiniTest::Unit::TestCase
307
277
  include CacheIncrementDecrementBehavior
308
278
 
309
279
  def setup
310
- @cache = ActiveSupport::Cache.lookup_store(:libmemcached_store, :expires_in => 60)
280
+ @cache = ActiveSupport::Cache.lookup_store(:libmemcached_store, expires_in: 60)
311
281
  @cache.clear
312
282
  @cache.silence!
313
283
  end
@@ -326,26 +296,34 @@ class LibmemcachedStoreTest < MiniTest::Unit::TestCase
326
296
  end
327
297
 
328
298
  def test_should_enable_consistent_ketema_hashing_by_default
329
- assert_equal :consistent_ketama, @cache.options[:distribution]
299
+ assert_equal :consistent_ketama, @cache.client_options[:distribution]
330
300
  end
331
301
 
332
302
  def test_should_not_enable_non_blocking_io_by_default
333
- assert_equal false, @cache.options[:no_block]
303
+ assert_equal false, @cache.client_options[:no_block]
334
304
  end
335
305
 
336
306
  def test_should_not_enable_server_failover_by_default
337
- assert_nil @cache.options[:failover]
307
+ assert_nil @cache.client_options[:failover]
338
308
  end
339
309
 
340
310
  def test_should_allow_configuration_of_custom_options
341
- options = {
342
- :tcp_nodelay => true,
343
- :distribution => :modula
344
- }
311
+ options = { client: { tcp_nodelay: true, distribution: :modula } }
345
312
 
346
313
  store = ActiveSupport::Cache.lookup_store :libmemcached_store, 'localhost', options
347
314
 
348
- assert_equal :modula, store.options[:distribution]
349
- assert_equal true, store.options[:tcp_nodelay]
315
+ assert_equal :modula, store.client_options[:distribution]
316
+ assert_equal true, store.client_options[:tcp_nodelay]
317
+ end
318
+
319
+ def test_should_allow_mute_and_silence
320
+ cache = ActiveSupport::Cache.lookup_store :libmemcached_store, 'localhost'
321
+ cache.mute do
322
+ assert cache.write('foo', 'bar')
323
+ assert_equal 'bar', cache.read('foo')
324
+ end
325
+ refute cache.silence?
326
+ cache.silence!
327
+ assert cache.silence?
350
328
  end
351
329
  end
@@ -0,0 +1,94 @@
1
+ require 'benchmark'
2
+ require 'active_support'
3
+
4
+ require 'libmemcached_store'
5
+ require 'active_support/cache/libmemcached_store'
6
+
7
+ require 'dalli'
8
+ require 'active_support/cache/dalli_store'
9
+
10
+ puts "Testing with"
11
+ puts RUBY_DESCRIPTION
12
+ puts "Dalli #{Dalli::VERSION}"
13
+ puts "Libmemcached_store #{LibmemcachedStore::VERSION}"
14
+
15
+ # We'll use a simple @value to try to avoid spending time in Marshal,
16
+ # which is a constant penalty that both clients have to pay
17
+ @value = []
18
+ @marshalled = Marshal.dump(@value)
19
+
20
+ @servers = ['127.0.0.1:11211']
21
+ @key1 = "Short"
22
+ @key2 = "Sym1-2-3::45"*4
23
+ @key3 = "Long"*40
24
+ @key4 = "Medium"*8
25
+
26
+ N = 2_500
27
+
28
+ @dalli = ActiveSupport::Cache::DalliStore.new(@servers).silence!
29
+ @libm = ActiveSupport::Cache::LibmemcachedStore.new(@servers).silence!
30
+
31
+ def clear
32
+ @dalli.clear
33
+ @libm.clear
34
+ end
35
+
36
+ def test_method(title, method_name, key, *arguments)
37
+ { dalli: @dalli, libm: @libm }.each do |name, store|
38
+ @job.report("#{title}:#{name}") { N.times { store.send(method_name, key, *arguments) } }
39
+ end
40
+ end
41
+
42
+ def run_method(method_name, key, *arguments)
43
+ [@dalli, @libm].each do |store|
44
+ store.send(method_name, key, *arguments)
45
+ end
46
+ end
47
+
48
+ Benchmark.bm(31) do |x|
49
+ @job = x
50
+
51
+ test_method('write:short', :write, @key1, @value)
52
+ test_method('write:long', :write, @key3, @value)
53
+ test_method('write:raw', :write, @key4, @value, raw: true)
54
+
55
+ puts
56
+ clear
57
+
58
+ test_method('read:miss', :read, @key1)
59
+ test_method('read:miss2', :read, @key1)
60
+
61
+ run_method(:write, @key4, @value)
62
+ test_method('read:exist', :read, @key4)
63
+
64
+ run_method(:write, @key4, @value, expires_in: 1)
65
+ sleep(1)
66
+ test_method('read:expired', :read, @key2)
67
+
68
+ run_method(:write, @key3, @value, raw: true)
69
+ test_method('read:raw', :read, @key3, raw: true)
70
+
71
+ puts
72
+ clear
73
+
74
+ test_method('exist:miss', :exist?, @key4)
75
+
76
+ run_method(:write, @key4, @value)
77
+ test_method('exist:hit', :exist?, @key4)
78
+
79
+ puts
80
+ clear
81
+
82
+ test_method('delete:miss', :delete, @key4)
83
+
84
+ run_method(:write, @key1, @value)
85
+ test_method('delete:hit', :delete, @key1)
86
+
87
+ puts
88
+ clear
89
+
90
+ run_method(:write, @key4, 0, raw: true)
91
+
92
+ test_method('increment', :increment, @key4)
93
+ test_method('decrement', :decrement, @key4)
94
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails3_libmemcached_store
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.5.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -11,7 +11,7 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2012-08-16 00:00:00.000000000 Z
14
+ date: 2012-09-11 00:00:00.000000000 Z
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
17
17
  name: memcached
@@ -120,6 +120,8 @@ extra_rdoc_files: []
120
120
  files:
121
121
  - .gitignore
122
122
  - .travis.yml
123
+ - BENCHMARKS
124
+ - CHANGELOG.md
123
125
  - Gemfile
124
126
  - MIT-LICENSE
125
127
  - README.md
@@ -128,15 +130,16 @@ files:
128
130
  - gemfiles/rails31.gemfile
129
131
  - gemfiles/rails32.gemfile
130
132
  - lib/action_dispatch/session/libmemcached_store.rb
131
- - lib/active_support/cache/compressed_libmemcached_store.rb
132
133
  - lib/active_support/cache/libmemcached_store.rb
133
134
  - lib/libmemcached_store.rb
135
+ - lib/memcached/get_with_flags.rb
134
136
  - lib/version.rb
135
137
  - libmemcached_store.gemspec
136
138
  - test/action_dispatch/abstract_unit.rb
137
139
  - test/action_dispatch/libmemcached_store_test.rb
138
140
  - test/active_support/libmemcached_store_test.rb
139
141
  - test/fixtures/session_autoload_test.rb
142
+ - test/profile/benchmark.rb
140
143
  - test/test_helper.rb
141
144
  homepage: http://github.com/ccocchi/libmemcached_store
142
145
  licenses: []
@@ -158,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
158
161
  version: '0'
159
162
  requirements: []
160
163
  rubyforge_project:
161
- rubygems_version: 1.8.23
164
+ rubygems_version: 1.8.21
162
165
  signing_key:
163
166
  specification_version: 3
164
167
  summary: ActiveSupport 3+ cache store for the C-based libmemcached client
@@ -167,4 +170,5 @@ test_files:
167
170
  - test/action_dispatch/libmemcached_store_test.rb
168
171
  - test/active_support/libmemcached_store_test.rb
169
172
  - test/fixtures/session_autoload_test.rb
173
+ - test/profile/benchmark.rb
170
174
  - test/test_helper.rb
@@ -1,15 +0,0 @@
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