dalli 2.7.4 → 2.7.5

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dalli might be problematic. Click here for more details.

checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 91e347f30ee656fe31027c0eab1b45885a2adbec
4
- data.tar.gz: 55755a2802168338fb9da8507ab8338436994bb6
3
+ metadata.gz: 29b86664473df52d0ca07fbf21015a39260962a1
4
+ data.tar.gz: 5061af271bef6e1db864d5e6de4f9464412e0c67
5
5
  SHA512:
6
- metadata.gz: 48faaa3c18f65110ec7f7bb826139d0df0ac4662b0b29458fe9a9bdc1b219aaaa838fd933b8c3ca4cf8d4201d68e5f73a9915721f3c5be62402379627bbcd024
7
- data.tar.gz: 31494501ba0963d4e6ff62998c9da03a9576659c69ddab17b5f916a0778a7fb0c6f1a3c38ec3c26d6b096fac5e88f9167c53aec911d8567ce42df980b29a1cfa
6
+ metadata.gz: 7ccec720347f06a142f74d1a500948390f0dcd3520c73922363267c393d99067cb3682f4c2577cc2cbbd99a18a93f800041eaec7570303c73d5e7ab451b9396e
7
+ data.tar.gz: a68483e89e28c82d52852d73c921aa40578abad55a2f3ec9106b01f1fd5dfc6f98104e9b2f8ab0006306685a4b3ecafca3d768ebf81223c213171482ee00d1f1
data/Gemfile CHANGED
@@ -2,11 +2,4 @@ source 'https://rubygems.org'
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'rake'
6
5
  gem 'kgio', :platform => :mri
7
- gem 'appraisal'
8
- gem 'connection_pool'
9
-
10
- group :test do
11
- gem 'simplecov'
12
- end
data/History.md CHANGED
@@ -1,6 +1,21 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
+ 2.7.5
5
+ ==========
6
+
7
+ - Support rcvbuff and sndbuff byte configuration. (btatnall)
8
+ - Add `:cache_nils` option to support nil values in `DalliStore#fetch` and `Dalli::Client#fetch` (wjordan, #559)
9
+ - Log retryable server errors with 'warn' instead of 'info' (phrinx)
10
+ - Fix timeout issue with Dalli::Client#get_multi_yielder (dspeterson)
11
+ - Escape namespaces with special regexp characters (Steven Peckins)
12
+ - Ensure LocalCache supports the `:raw` option and Entry unwrapping (sj26)
13
+ - Ensure bad ttl values don't cause Dalli::RingError (eagletmt, petergoldstein)
14
+ - Always pass namespaced key to instrumentation API (kaorimatz)
15
+ - Replace use of deprecated TimeoutError with Timeout::Error (eagletmt)
16
+ - Clean up gemspec, and use Bundler for loading (grosser)
17
+ - Dry up local cache testing (grosser)
18
+
4
19
  2.7.4
5
20
  ==========
6
21
 
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) Mike Perham
1
+ Copyright (c) Peter M. Goldstein, Mike Perham
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- Dalli [![Build Status](https://secure.travis-ci.org/mperham/dalli.png)](http://travis-ci.org/mperham/dalli) [![Dependency Status](https://gemnasium.com/mperham/dalli.png)](https://gemnasium.com/mperham/dalli) [![Code Climate](https://codeclimate.com/github/mperham/dalli.png)](https://codeclimate.com/github/mperham/dalli)
1
+ Dalli [![Build Status](https://secure.travis-ci.org/petergoldstein/dalli.png)](http://travis-ci.org/petergoldstein/dalli) [![Dependency Status](https://gemnasium.com/petergoldstein/dalli.png)](https://gemnasium.com/petergoldstein/dalli) [![Code Climate](https://codeclimate.com/github/petergoldstein/dalli.png)](https://codeclimate.com/github/petergoldstein/dalli)
2
2
  =====
3
3
 
4
4
  Dalli is a high performance pure Ruby client for accessing memcached servers. It works with memcached 1.4+ only as it uses the newer binary protocol. It should be considered a replacement for the memcache-client gem.
@@ -13,7 +13,7 @@ Dalli's initial development was sponsored by [CouchBase](http://www.couchbase.co
13
13
  Design
14
14
  ------------
15
15
 
16
- I decided to write Dalli after maintaining memcache-client for two years for a few specific reasons:
16
+ Mike Perham decided to write Dalli after maintaining memcache-client for two years for a few specific reasons:
17
17
 
18
18
  0. The code is mostly old and gross. The bulk of the code is a single 1000 line .rb file.
19
19
  1. It has a lot of options that are infrequently used which complicate the codebase.
@@ -186,6 +186,12 @@ If serving compressed data using nginx's HttpMemcachedModule, set `memcached_gzi
186
186
 
187
187
  **password**: The password to use for authenticating this client instance against a SASL-enabled memcached server. Heroku users should not need to use this normally.
188
188
 
189
+ **sndbuf**: In bytes, set the socket SO_SNDBUF. Defaults to operating system default.
190
+
191
+ **rcvbuf**: In bytes, set the socket SO_RCVBUF. Defaults to operating system default.
192
+
193
+ **cache_nils**: Boolean. If true Dalli will not treat cached `nil` values as 'not found' for `#fetch` operations. Default is false.
194
+
189
195
  Features and Changes
190
196
  ------------------------
191
197
 
@@ -209,20 +215,22 @@ We're not accepting new compressors. They are trivial to add in an initializer.
209
215
  Thanks
210
216
  ------------
211
217
 
218
+ Mike Perham - for originally authoring the Dalli project and serving as maintainer and primary contributor
219
+
212
220
  Eric Wong - for help using his [kgio](http://bogomips.org/kgio/) library.
213
221
 
214
222
  Brian Mitchell - for his remix-stash project which was helpful when implementing and testing the binary protocol support.
215
223
 
216
224
  [CouchBase](http://couchbase.com) - for their project sponsorship
217
225
 
218
-
219
- Author
226
+ Authors
220
227
  ----------
221
228
 
222
- Mike Perham, [mikeperham.com](http://mikeperham.com), [@mperham](http://twitter.com/mperham)
229
+ * [Peter M. Goldstein](https://github.com/petergoldstein) - current maintainer
230
+ * [Mike Perham](https://github.com/mperham) and contributors
223
231
 
224
232
 
225
233
  Copyright
226
234
  -----------
227
235
 
228
- Copyright (c) Mike Perham. See LICENSE for details.
236
+ Copyright (c) Mike Perham, Peter M. Goldstein. See LICENSE for details.
@@ -68,6 +68,7 @@ module ActiveSupport
68
68
  end
69
69
 
70
70
  extend Strategy::LocalCache
71
+ extend LocalCacheEntryUnwrapAndRaw
71
72
  end
72
73
 
73
74
  ##
@@ -81,31 +82,41 @@ module ActiveSupport
81
82
  @data.with(&block)
82
83
  end
83
84
 
85
+ # Fetch the value associated with the key.
86
+ # If a value is found, then it is returned.
87
+ #
88
+ # If a value is not found and no block is given, then nil is returned.
89
+ #
90
+ # If a value is not found (or if the found value is nil and :cache_nils is false)
91
+ # and a block is given, the block will be invoked and its return value
92
+ # written to the cache and returned.
84
93
  def fetch(name, options=nil)
85
94
  options ||= {}
95
+ options[:cache_nils] = true if @options[:cache_nils]
86
96
  namespaced_name = namespaced_key(name, options)
87
-
97
+ not_found = options[:cache_nils] ? Dalli::Server::NOT_FOUND : nil
88
98
  if block_given?
99
+ entry = not_found
89
100
  unless options[:force]
90
101
  entry = instrument(:read, namespaced_name, options) do |payload|
91
102
  read_entry(namespaced_name, options).tap do |result|
92
103
  if payload
93
104
  payload[:super_operation] = :fetch
94
- payload[:hit] = !result.nil?
105
+ payload[:hit] = result != not_found
95
106
  end
96
107
  end
97
108
  end
98
109
  end
99
110
 
100
- if !entry.nil?
101
- instrument(:fetch_hit, name, options) { |payload| }
102
- entry
103
- else
104
- result = instrument(:generate, name, options) do |payload|
111
+ if entry == not_found
112
+ result = instrument(:generate, namespaced_name, options) do |payload|
105
113
  yield
106
114
  end
107
115
  write(name, result, options)
108
116
  result
117
+ else
118
+ instrument(:fetch_hit, namespaced_name, options) { |payload| }
119
+ entry
109
120
  end
110
121
  else
111
122
  read(name, options)
@@ -157,7 +168,7 @@ module ActiveSupport
157
168
  def read_multi(*names)
158
169
  options = names.extract_options!
159
170
  mapping = names.inject({}) { |memo, name| memo[namespaced_key(name, options)] = name; memo }
160
- instrument(:read_multi, names) do
171
+ instrument(:read_multi, mapping.keys) do
161
172
  results = {}
162
173
  if local_cache
163
174
  mapping.each_key do |key|
@@ -188,7 +199,7 @@ module ActiveSupport
188
199
  options = names.extract_options!
189
200
  mapping = names.inject({}) { |memo, name| memo[namespaced_key(name, options)] = name; memo }
190
201
 
191
- instrument(:fetch_multi, names) do
202
+ instrument(:fetch_multi, mapping.keys) do
192
203
  with do |connection|
193
204
  results = connection.get_multi(mapping.keys)
194
205
 
@@ -367,6 +378,25 @@ module ActiveSupport
367
378
  def raise_errors?
368
379
  !!@options[:raise_errors]
369
380
  end
381
+
382
+ # Make sure LocalCache is giving raw values, not `Entry`s, and
383
+ # respect `raw` option.
384
+ module LocalCacheEntryUnwrapAndRaw # :nodoc:
385
+ protected
386
+ def read_entry(key, options)
387
+ retval = super
388
+ if retval.is_a? ActiveSupport::Cache::Entry
389
+ # Must have come from LocalStore, unwrap it
390
+ if options[:raw]
391
+ retval.value.to_s
392
+ else
393
+ retval.value
394
+ end
395
+ else
396
+ retval
397
+ end
398
+ end
399
+ end
370
400
  end
371
401
  end
372
402
  end
@@ -27,6 +27,7 @@ module Dalli
27
27
  # - :compress - defaults to false, if true Dalli will compress values larger than 1024 bytes before sending them to memcached.
28
28
  # - :serializer - defaults to Marshal
29
29
  # - :compressor - defaults to zlib
30
+ # - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for #fetch operations.
30
31
  #
31
32
  def initialize(servers=nil, options={})
32
33
  @servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || '127.0.0.1:11211')
@@ -52,8 +53,9 @@ module Dalli
52
53
 
53
54
  ##
54
55
  # Get the value associated with the key.
56
+ # If a value is not found, then +nil+ is returned.
55
57
  def get(key, options=nil)
56
- perform(:get, key)
58
+ perform(:get, key, options)
57
59
  end
58
60
 
59
61
  ##
@@ -71,12 +73,25 @@ module Dalli
71
73
  end
72
74
  end
73
75
 
76
+ CACHE_NILS = {cache_nils: true}.freeze
77
+
78
+ # Fetch the value associated with the key.
79
+ # If a value is found, then it is returned.
80
+ #
81
+ # If a value is not found and no block is given, then nil is returned.
82
+ #
83
+ # If a value is not found (or if the found value is nil and :cache_nils is false)
84
+ # and a block is given, the block will be invoked and its return value
85
+ # written to the cache and returned.
74
86
  def fetch(key, ttl=nil, options=nil)
75
- ttl ||= @options[:expires_in].to_i
87
+ options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
76
88
  val = get(key, options)
77
- if val.nil? && block_given?
89
+ not_found = @options[:cache_nils] ?
90
+ val == Dalli::Server::NOT_FOUND :
91
+ val.nil?
92
+ if not_found && block_given?
78
93
  val = yield
79
- add(key, val, ttl, options)
94
+ add(key, val, ttl_or_default(ttl), options)
80
95
  end
81
96
  val
82
97
  end
@@ -93,34 +108,30 @@ module Dalli
93
108
  # - false if the value was changed by someone else.
94
109
  # - true if the value was successfully updated.
95
110
  def cas(key, ttl=nil, options=nil)
96
- ttl ||= @options[:expires_in].to_i
97
111
  (value, cas) = perform(:cas, key)
98
112
  value = (!value || value == 'Not found') ? nil : value
99
113
  if value
100
114
  newvalue = yield(value)
101
- perform(:set, key, newvalue, ttl, cas, options)
115
+ perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
102
116
  end
103
117
  end
104
118
 
105
119
  def set(key, value, ttl=nil, options=nil)
106
- ttl ||= @options[:expires_in].to_i
107
- perform(:set, key, value, ttl, 0, options)
120
+ perform(:set, key, value, ttl_or_default(ttl), 0, options)
108
121
  end
109
122
 
110
123
  ##
111
124
  # Conditionally add a key/value pair, if the key does not already exist
112
125
  # on the server. Returns truthy if the operation succeeded.
113
126
  def add(key, value, ttl=nil, options=nil)
114
- ttl ||= @options[:expires_in].to_i
115
- perform(:add, key, value, ttl, options)
127
+ perform(:add, key, value, ttl_or_default(ttl), options)
116
128
  end
117
129
 
118
130
  ##
119
131
  # Conditionally add a key/value pair, only if the key already exists
120
132
  # on the server. Returns truthy if the operation succeeded.
121
133
  def replace(key, value, ttl=nil, options=nil)
122
- ttl ||= @options[:expires_in].to_i
123
- perform(:replace, key, value, ttl, 0, options)
134
+ perform(:replace, key, value, ttl_or_default(ttl), 0, options)
124
135
  end
125
136
 
126
137
  def delete(key)
@@ -161,8 +172,7 @@ module Dalli
161
172
  # #cas.
162
173
  def incr(key, amt=1, ttl=nil, default=nil)
163
174
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
164
- ttl ||= @options[:expires_in].to_i
165
- perform(:incr, key, amt.to_i, ttl, default)
175
+ perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
166
176
  end
167
177
 
168
178
  ##
@@ -181,8 +191,7 @@ module Dalli
181
191
  # #cas.
182
192
  def decr(key, amt=1, ttl=nil, default=nil)
183
193
  raise ArgumentError, "Positive values only: #{amt}" if amt < 0
184
- ttl ||= @options[:expires_in].to_i
185
- perform(:decr, key, amt.to_i, ttl, default)
194
+ perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
186
195
  end
187
196
 
188
197
  ##
@@ -190,8 +199,7 @@ module Dalli
190
199
  #
191
200
  # Returns true if key exists, otherwise nil.
192
201
  def touch(key, ttl=nil)
193
- ttl ||= @options[:expires_in].to_i
194
- resp = perform(:touch, key, ttl)
202
+ resp = perform(:touch, key, ttl_or_default(ttl))
195
203
  resp.nil? ? nil : true
196
204
  end
197
205
 
@@ -250,6 +258,12 @@ module Dalli
250
258
 
251
259
  private
252
260
 
261
+ def ttl_or_default(ttl)
262
+ (ttl || @options[:expires_in]).to_i
263
+ rescue NoMethodError
264
+ raise ArgumentError, "Cannot convert ttl (#{ttl}) to an integer"
265
+ end
266
+
253
267
  def groups_for_keys(*keys)
254
268
  groups = mapped_keys(keys).flatten.group_by do |key|
255
269
  begin
@@ -296,8 +310,9 @@ module Dalli
296
310
  end
297
311
 
298
312
  ##
299
- # Normalizes the argument into an array of servers. If the argument is a string, it's expected to be of
300
- # the format "memcache1.example.com:11211[,memcache2.example.com:11211[,memcache3.example.com:11211[...]]]
313
+ # Normalizes the argument into an array of servers.
314
+ # If the argument is a string, it's expected that the URIs are comma separated e.g.
315
+ # "memcache1.example.com:11211,memcache2.example.com:11211,memcache3.example.com:11211"
301
316
  def normalize_servers(servers)
302
317
  if servers.is_a? String
303
318
  return servers.split(",")
@@ -354,7 +369,7 @@ module Dalli
354
369
  end
355
370
 
356
371
  def key_without_namespace(key)
357
- (ns = namespace) ? key.sub(%r(\A#{ns}:), '') : key
372
+ (ns = namespace) ? key.sub(%r(\A#{Regexp.escape ns}:), '') : key
358
373
  end
359
374
 
360
375
  def namespace
@@ -401,12 +416,10 @@ module Dalli
401
416
  # calculate remaining timeout
402
417
  elapsed = Time.now - start
403
418
  timeout = servers.first.options[:socket_timeout]
404
- if elapsed > timeout
405
- readable = nil
406
- else
407
- sockets = servers.map(&:sock)
408
- readable, _ = IO.select(sockets, nil, nil, timeout - elapsed)
409
- end
419
+ time_left = (elapsed > timeout) ? 0 : timeout - elapsed
420
+
421
+ sockets = servers.map(&:sock)
422
+ readable, _ = IO.select(sockets, nil, nil, time_left)
410
423
 
411
424
  if readable.nil?
412
425
  # no response within timeout; abort pending connections
@@ -31,7 +31,11 @@ module Dalli
31
31
  :serializer => Marshal,
32
32
  :username => nil,
33
33
  :password => nil,
34
- :keepalive => true
34
+ :keepalive => true,
35
+ # max byte size for SO_SNDBUF
36
+ :sndbuf => nil,
37
+ # max byte size for SO_RCVBUF
38
+ :rcvbuf => nil
35
39
  }
36
40
 
37
41
  def initialize(attribs, options = {})
@@ -203,20 +207,28 @@ module Dalli
203
207
 
204
208
  def verify_state
205
209
  failure!(RuntimeError.new('Already writing to socket')) if @inprogress
206
- failure!(RuntimeError.new('Cannot share client between multiple processes')) if @pid && @pid != Process.pid
210
+ if @pid && @pid != Process.pid
211
+ message = 'Fork detected, re-connecting child process...'
212
+ Dalli.logger.info { message }
213
+ reconnect! message
214
+ end
215
+ end
216
+
217
+ def reconnect!(message)
218
+ close
219
+ sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
220
+ raise Dalli::NetworkError, message
207
221
  end
208
222
 
209
223
  def failure!(exception)
210
224
  message = "#{name} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
211
- Dalli.logger.info { message }
225
+ Dalli.logger.warn { message }
212
226
 
213
227
  @fail_count += 1
214
228
  if @fail_count >= options[:socket_max_failures]
215
229
  down!
216
230
  else
217
- close
218
- sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
219
- raise Dalli::NetworkError, "Socket operation failed, retrying..."
231
+ reconnect! 'Socket operation failed, retrying...'
220
232
  end
221
233
  end
222
234
 
@@ -255,10 +267,10 @@ module Dalli
255
267
  Thread.current[:dalli_multi]
256
268
  end
257
269
 
258
- def get(key)
270
+ def get(key, options=nil)
259
271
  req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
260
272
  write(req)
261
- generic_response(true)
273
+ generic_response(true, !!(options && options.is_a?(Hash) && options[:cache_nils]))
262
274
  end
263
275
 
264
276
  def send_multiget(keys)
@@ -400,7 +412,7 @@ module Dalli
400
412
  marshalled = true
401
413
  begin
402
414
  self.serializer.dump(value)
403
- rescue TimeoutError => e
415
+ rescue Timeout::Error => e
404
416
  raise e
405
417
  rescue => ex
406
418
  # Marshalling can throw several different types of generic Ruby exceptions.
@@ -413,7 +425,8 @@ module Dalli
413
425
  value.to_s
414
426
  end
415
427
  compressed = false
416
- if @options[:compress] && value.bytesize >= @options[:compression_min_size] &&
428
+ set_compress_option = true if options && options[:compress]
429
+ if (@options[:compress] || set_compress_option) && value.bytesize >= @options[:compression_min_size] &&
417
430
  (!@options[:compression_max_size] || value.bytesize <= @options[:compression_max_size])
418
431
  value = self.compressor.compress(value)
419
432
  compressed = true
@@ -471,19 +484,21 @@ module Dalli
471
484
  # > An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a unix timestamp of an exact date.
472
485
  MAX_ACCEPTABLE_EXPIRATION_INTERVAL = 30*24*60*60 # 30 days
473
486
  def sanitize_ttl(ttl)
474
- if ttl > MAX_ACCEPTABLE_EXPIRATION_INTERVAL
475
- Dalli.logger.debug "Expiration interval too long for Memcached, converting to an expiration timestamp"
476
- Time.now.to_i + ttl.to_i
477
- else
478
- ttl.to_i
479
- end
487
+ ttl_as_i = ttl.to_i
488
+ return ttl_as_i if ttl_as_i <= MAX_ACCEPTABLE_EXPIRATION_INTERVAL
489
+ Dalli.logger.debug "Expiration interval (#{ttl_as_i}) too long for Memcached, converting to an expiration timestamp"
490
+ Time.now.to_i + ttl_as_i
480
491
  end
481
492
 
482
- def generic_response(unpack=false)
493
+ # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
494
+ class NilObject; end
495
+ NOT_FOUND = NilObject.new
496
+
497
+ def generic_response(unpack=false, cache_nils=false)
483
498
  (extras, _, status, count) = read_header.unpack(NORMAL_HEADER)
484
499
  data = read(count) if count > 0
485
500
  if status == 1
486
- nil
501
+ cache_nils ? NOT_FOUND : nil
487
502
  elsif status == 2 || status == 5
488
503
  false # Not stored, normal status for add operation
489
504
  elsif status != 0