rails3_libmemcached_store 0.4.0 → 0.5.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/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