dalli 2.7.11 → 3.0.0
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 +4 -4
- data/Gemfile +5 -10
- data/History.md +28 -0
- data/README.md +17 -77
- data/lib/dalli/cas/client.rb +1 -59
- data/lib/dalli/client.rb +165 -107
- data/lib/dalli/compressor.rb +4 -3
- data/lib/dalli/options.rb +3 -4
- data/lib/dalli/protocol/binary.rb +753 -0
- data/lib/dalli/protocol.rb +9 -0
- data/lib/dalli/ring.rb +12 -63
- data/lib/dalli/server.rb +2 -748
- data/lib/dalli/socket.rb +59 -134
- data/lib/dalli/version.rb +2 -1
- data/lib/dalli.rb +14 -16
- data/lib/rack/session/dalli.rb +21 -40
- metadata +48 -8
- data/lib/action_dispatch/middleware/session/dalli_store.rb +0 -82
- data/lib/active_support/cache/dalli_store.rb +0 -441
- data/lib/dalli/railtie.rb +0 -8
data/lib/dalli/client.rb
CHANGED
@@ -1,11 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require
|
2
|
+
|
3
|
+
require "digest/md5"
|
4
|
+
require "set"
|
4
5
|
|
5
6
|
# encoding: ascii
|
6
7
|
module Dalli
|
7
8
|
class Client
|
8
|
-
|
9
9
|
##
|
10
10
|
# Dalli::Client is the main class which developers will use to interact with
|
11
11
|
# the memcached server. Usage:
|
@@ -30,9 +30,11 @@ module Dalli
|
|
30
30
|
# - :compressor - defaults to zlib
|
31
31
|
# - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for #fetch operations.
|
32
32
|
# - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method, useful for injecting a FIPS compliant hash object.
|
33
|
+
# - :protocol_implementation - defaults to Dalli::Protocol::Binary which uses the binary protocol. Allows you to pass an alternative implementation using another protocol.
|
33
34
|
#
|
34
|
-
def initialize(servers=nil, options={})
|
35
|
-
|
35
|
+
def initialize(servers = nil, options = {})
|
36
|
+
validate_servers_arg(servers)
|
37
|
+
@servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || "127.0.0.1:11211")
|
36
38
|
@options = normalize_options(options)
|
37
39
|
@ring = nil
|
38
40
|
end
|
@@ -56,7 +58,7 @@ module Dalli
|
|
56
58
|
##
|
57
59
|
# Get the value associated with the key.
|
58
60
|
# If a value is not found, then +nil+ is returned.
|
59
|
-
def get(key, options=nil)
|
61
|
+
def get(key, options = nil)
|
60
62
|
perform(:get, key, options)
|
61
63
|
end
|
62
64
|
|
@@ -65,15 +67,15 @@ module Dalli
|
|
65
67
|
# If a block is given, yields key/value pairs one at a time.
|
66
68
|
# Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
|
67
69
|
def get_multi(*keys)
|
68
|
-
|
69
|
-
|
70
|
+
keys.flatten!
|
71
|
+
keys.compact!
|
70
72
|
|
71
|
-
return {} if
|
73
|
+
return {} if keys.empty?
|
72
74
|
if block_given?
|
73
|
-
get_multi_yielder(keys) {|k, data| yield k, data.first}
|
75
|
+
get_multi_yielder(keys) { |k, data| yield k, data.first }
|
74
76
|
else
|
75
|
-
|
76
|
-
get_multi_yielder(keys) {|k, data| hash[k] = data.first}
|
77
|
+
{}.tap do |hash|
|
78
|
+
get_multi_yielder(keys) { |k, data| hash[k] = data.first }
|
77
79
|
end
|
78
80
|
end
|
79
81
|
end
|
@@ -88,11 +90,11 @@ module Dalli
|
|
88
90
|
# If a value is not found (or if the found value is nil and :cache_nils is false)
|
89
91
|
# and a block is given, the block will be invoked and its return value
|
90
92
|
# written to the cache and returned.
|
91
|
-
def fetch(key, ttl=nil, options=nil)
|
93
|
+
def fetch(key, ttl = nil, options = nil)
|
92
94
|
options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
|
93
95
|
val = get(key, options)
|
94
96
|
not_found = @options[:cache_nils] ?
|
95
|
-
val == Dalli::
|
97
|
+
val == Dalli::Protocol::NOT_FOUND :
|
96
98
|
val.nil?
|
97
99
|
if not_found && block_given?
|
98
100
|
val = yield
|
@@ -112,7 +114,7 @@ module Dalli
|
|
112
114
|
# - nil if the key did not exist.
|
113
115
|
# - false if the value was changed by someone else.
|
114
116
|
# - true if the value was successfully updated.
|
115
|
-
def cas(key, ttl=nil, options=nil, &block)
|
117
|
+
def cas(key, ttl = nil, options = nil, &block)
|
116
118
|
cas_core(key, false, ttl, options, &block)
|
117
119
|
end
|
118
120
|
|
@@ -123,25 +125,25 @@ module Dalli
|
|
123
125
|
# Returns:
|
124
126
|
# - false if the value was changed by someone else.
|
125
127
|
# - true if the value was successfully updated.
|
126
|
-
def cas!(key, ttl=nil, options=nil, &block)
|
128
|
+
def cas!(key, ttl = nil, options = nil, &block)
|
127
129
|
cas_core(key, true, ttl, options, &block)
|
128
130
|
end
|
129
131
|
|
130
|
-
def set(key, value, ttl=nil, options=nil)
|
132
|
+
def set(key, value, ttl = nil, options = nil)
|
131
133
|
perform(:set, key, value, ttl_or_default(ttl), 0, options)
|
132
134
|
end
|
133
135
|
|
134
136
|
##
|
135
137
|
# Conditionally add a key/value pair, if the key does not already exist
|
136
138
|
# on the server. Returns truthy if the operation succeeded.
|
137
|
-
def add(key, value, ttl=nil, options=nil)
|
139
|
+
def add(key, value, ttl = nil, options = nil)
|
138
140
|
perform(:add, key, value, ttl_or_default(ttl), options)
|
139
141
|
end
|
140
142
|
|
141
143
|
##
|
142
144
|
# Conditionally add a key/value pair, only if the key already exists
|
143
145
|
# on the server. Returns truthy if the operation succeeded.
|
144
|
-
def replace(key, value, ttl=nil, options=nil)
|
146
|
+
def replace(key, value, ttl = nil, options = nil)
|
145
147
|
perform(:replace, key, value, ttl_or_default(ttl), 0, options)
|
146
148
|
end
|
147
149
|
|
@@ -163,7 +165,7 @@ module Dalli
|
|
163
165
|
perform(:prepend, key, value.to_s)
|
164
166
|
end
|
165
167
|
|
166
|
-
def flush(delay=0)
|
168
|
+
def flush(delay = 0)
|
167
169
|
time = -delay
|
168
170
|
ring.servers.map { |s| s.request(:flush, time += delay) }
|
169
171
|
end
|
@@ -181,7 +183,7 @@ module Dalli
|
|
181
183
|
# Note that the ttl will only apply if the counter does not already
|
182
184
|
# exist. To increase an existing counter and update its TTL, use
|
183
185
|
# #cas.
|
184
|
-
def incr(key, amt=1, ttl=nil, default=nil)
|
186
|
+
def incr(key, amt = 1, ttl = nil, default = nil)
|
185
187
|
raise ArgumentError, "Positive values only: #{amt}" if amt < 0
|
186
188
|
perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
|
187
189
|
end
|
@@ -200,7 +202,7 @@ module Dalli
|
|
200
202
|
# Note that the ttl will only apply if the counter does not already
|
201
203
|
# exist. To decrease an existing counter and update its TTL, use
|
202
204
|
# #cas.
|
203
|
-
def decr(key, amt=1, ttl=nil, default=nil)
|
205
|
+
def decr(key, amt = 1, ttl = nil, default = nil)
|
204
206
|
raise ArgumentError, "Positive values only: #{amt}" if amt < 0
|
205
207
|
perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
|
206
208
|
end
|
@@ -209,20 +211,28 @@ module Dalli
|
|
209
211
|
# Touch updates expiration time for a given key.
|
210
212
|
#
|
211
213
|
# Returns true if key exists, otherwise nil.
|
212
|
-
def touch(key, ttl=nil)
|
214
|
+
def touch(key, ttl = nil)
|
213
215
|
resp = perform(:touch, key, ttl_or_default(ttl))
|
214
216
|
resp.nil? ? nil : true
|
215
217
|
end
|
216
218
|
|
219
|
+
##
|
220
|
+
# Gat (get and touch) fetch an item and simultaneously update its expiration time.
|
221
|
+
#
|
222
|
+
# If a value is not found, then +nil+ is returned.
|
223
|
+
def gat(key, ttl = nil)
|
224
|
+
perform(:gat, key, ttl_or_default(ttl))
|
225
|
+
end
|
226
|
+
|
217
227
|
##
|
218
228
|
# Collect the stats for each server.
|
219
229
|
# You can optionally pass a type including :items, :slabs or :settings to get specific stats
|
220
230
|
# Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
|
221
|
-
def stats(type=nil)
|
222
|
-
type = nil
|
231
|
+
def stats(type = nil)
|
232
|
+
type = nil unless [nil, :items, :slabs, :settings].include? type
|
223
233
|
values = {}
|
224
234
|
ring.servers.each do |server|
|
225
|
-
values[
|
235
|
+
values[server.name.to_s] = server.alive? ? server.request(:stats, type.to_s) : nil
|
226
236
|
end
|
227
237
|
values
|
228
238
|
end
|
@@ -246,11 +256,63 @@ module Dalli
|
|
246
256
|
def version
|
247
257
|
values = {}
|
248
258
|
ring.servers.each do |server|
|
249
|
-
values[
|
259
|
+
values[server.name.to_s] = server.alive? ? server.request(:version) : nil
|
250
260
|
end
|
251
261
|
values
|
252
262
|
end
|
253
263
|
|
264
|
+
##
|
265
|
+
# Get the value and CAS ID associated with the key. If a block is provided,
|
266
|
+
# value and CAS will be passed to the block.
|
267
|
+
def get_cas(key)
|
268
|
+
(value, cas) = perform(:cas, key)
|
269
|
+
value = !value || value == "Not found" ? nil : value
|
270
|
+
if block_given?
|
271
|
+
yield value, cas
|
272
|
+
else
|
273
|
+
[value, cas]
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
##
|
278
|
+
# Fetch multiple keys efficiently, including available metadata such as CAS.
|
279
|
+
# If a block is given, yields key/data pairs one a time. Data is an array:
|
280
|
+
# [value, cas_id]
|
281
|
+
# If no block is given, returns a hash of
|
282
|
+
# { 'key' => [value, cas_id] }
|
283
|
+
def get_multi_cas(*keys)
|
284
|
+
if block_given?
|
285
|
+
get_multi_yielder(keys) { |*args| yield(*args) }
|
286
|
+
else
|
287
|
+
{}.tap do |hash|
|
288
|
+
get_multi_yielder(keys) { |k, data| hash[k] = data }
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
##
|
294
|
+
# Set the key-value pair, verifying existing CAS.
|
295
|
+
# Returns the resulting CAS value if succeeded, and falsy otherwise.
|
296
|
+
def set_cas(key, value, cas, ttl = nil, options = nil)
|
297
|
+
ttl ||= @options[:expires_in].to_i
|
298
|
+
perform(:set, key, value, ttl, cas, options)
|
299
|
+
end
|
300
|
+
|
301
|
+
##
|
302
|
+
# Conditionally add a key/value pair, verifying existing CAS, only if the
|
303
|
+
# key already exists on the server. Returns the new CAS value if the
|
304
|
+
# operation succeeded, or falsy otherwise.
|
305
|
+
def replace_cas(key, value, cas, ttl = nil, options = nil)
|
306
|
+
ttl ||= @options[:expires_in].to_i
|
307
|
+
perform(:replace, key, value, ttl, cas, options)
|
308
|
+
end
|
309
|
+
|
310
|
+
# Delete a key/value pair, verifying existing CAS.
|
311
|
+
# Returns true if succeeded, and falsy otherwise.
|
312
|
+
def delete_cas(key, cas = 0)
|
313
|
+
perform(:delete, key, cas)
|
314
|
+
end
|
315
|
+
|
254
316
|
##
|
255
317
|
# Close our connection to each server.
|
256
318
|
# If you perform another operation after this, the connections will be re-established.
|
@@ -262,16 +324,11 @@ module Dalli
|
|
262
324
|
end
|
263
325
|
alias_method :reset, :close
|
264
326
|
|
265
|
-
# Stub method so a bare Dalli client can pretend to be a connection pool.
|
266
|
-
def with
|
267
|
-
yield self
|
268
|
-
end
|
269
|
-
|
270
327
|
private
|
271
328
|
|
272
|
-
def cas_core(key, always_set, ttl=nil, options=nil)
|
329
|
+
def cas_core(key, always_set, ttl = nil, options = nil)
|
273
330
|
(value, cas) = perform(:cas, key)
|
274
|
-
value =
|
331
|
+
value = !value || value == "Not found" ? nil : value
|
275
332
|
return if value.nil? && !always_set
|
276
333
|
newvalue = yield(value)
|
277
334
|
perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
|
@@ -284,35 +341,29 @@ module Dalli
|
|
284
341
|
end
|
285
342
|
|
286
343
|
def groups_for_keys(*keys)
|
287
|
-
|
344
|
+
keys.flatten!
|
345
|
+
keys.map! { |a| validate_key(a.to_s) }
|
346
|
+
|
347
|
+
keys.group_by { |key|
|
288
348
|
begin
|
289
349
|
ring.server_for_key(key)
|
290
350
|
rescue Dalli::RingError
|
291
351
|
Dalli.logger.debug { "unable to get key #{key}" }
|
292
352
|
nil
|
293
353
|
end
|
294
|
-
|
295
|
-
return groups
|
296
|
-
end
|
297
|
-
|
298
|
-
def mapped_keys(keys)
|
299
|
-
keys_array = keys.flatten
|
300
|
-
keys_array.map! { |a| validate_key(a.to_s) }
|
301
|
-
keys_array
|
354
|
+
}
|
302
355
|
end
|
303
356
|
|
304
357
|
def make_multi_get_requests(groups)
|
305
358
|
groups.each do |server, keys_for_server|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
315
|
-
end
|
359
|
+
# TODO: do this with the perform chokepoint?
|
360
|
+
# But given the fact that fetching the response doesn't take place
|
361
|
+
# in that slot it's misleading anyway. Need to move all of this method
|
362
|
+
# into perform to be meaningful
|
363
|
+
server.request(:send_multiget, keys_for_server)
|
364
|
+
rescue DalliError, NetworkError => e
|
365
|
+
Dalli.logger.debug { e.inspect }
|
366
|
+
Dalli.logger.debug { "unable to get keys for server #{server.name}" }
|
316
367
|
end
|
317
368
|
end
|
318
369
|
|
@@ -330,6 +381,16 @@ module Dalli
|
|
330
381
|
servers
|
331
382
|
end
|
332
383
|
|
384
|
+
##
|
385
|
+
# Ensures that the servers arg is either an array or a string.
|
386
|
+
def validate_servers_arg(servers)
|
387
|
+
return if servers.nil?
|
388
|
+
return if servers.is_a?(Array)
|
389
|
+
return if servers.is_a?(String)
|
390
|
+
|
391
|
+
raise ArgumentError, "An explicit servers argument must be a comma separated string or an array containing strings."
|
392
|
+
end
|
393
|
+
|
333
394
|
##
|
334
395
|
# Normalizes the argument into an array of servers.
|
335
396
|
# If the argument is a string, or an array containing strings, it's expected that the URIs are comma separated e.g.
|
@@ -346,30 +407,29 @@ module Dalli
|
|
346
407
|
|
347
408
|
def ring
|
348
409
|
@ring ||= Dalli::Ring.new(
|
349
|
-
@servers.map
|
350
|
-
|
351
|
-
if s
|
410
|
+
@servers.map { |s|
|
411
|
+
server_options = {}
|
412
|
+
if s.start_with?("memcached://")
|
352
413
|
uri = URI.parse(s)
|
353
414
|
server_options[:username] = uri.user
|
354
415
|
server_options[:password] = uri.password
|
355
416
|
s = "#{uri.host}:#{uri.port}"
|
356
417
|
end
|
357
|
-
Dalli::
|
358
|
-
|
418
|
+
@options.fetch(:protocol_implementation, Dalli::Protocol::Binary).new(s, @options.merge(server_options))
|
419
|
+
}, @options
|
359
420
|
)
|
360
421
|
end
|
361
422
|
|
362
423
|
# Chokepoint method for instrumentation
|
363
424
|
def perform(*all_args)
|
364
425
|
return yield if block_given?
|
365
|
-
op, key, *args =
|
426
|
+
op, key, *args = all_args
|
366
427
|
|
367
428
|
key = key.to_s
|
368
429
|
key = validate_key(key)
|
369
430
|
begin
|
370
431
|
server = ring.server_for_key(key)
|
371
|
-
|
372
|
-
ret
|
432
|
+
server.request(op, key, *args)
|
373
433
|
rescue NetworkError => e
|
374
434
|
Dalli.logger.debug { e.inspect }
|
375
435
|
Dalli.logger.debug { "retrying request with new server" }
|
@@ -382,10 +442,10 @@ module Dalli
|
|
382
442
|
key = key_with_namespace(key)
|
383
443
|
if key.length > 250
|
384
444
|
digest_class = @options[:digest_class] || ::Digest::MD5
|
385
|
-
max_length_before_namespace = 212 - (namespace ||
|
445
|
+
max_length_before_namespace = 212 - (namespace || "").size
|
386
446
|
key = "#{key[0, max_length_before_namespace]}:md5:#{digest_class.hexdigest(key)}"
|
387
447
|
end
|
388
|
-
|
448
|
+
key
|
389
449
|
end
|
390
450
|
|
391
451
|
def key_with_namespace(key)
|
@@ -393,7 +453,7 @@ module Dalli
|
|
393
453
|
end
|
394
454
|
|
395
455
|
def key_without_namespace(key)
|
396
|
-
(ns = namespace) ? key.sub(%r
|
456
|
+
(ns = namespace) ? key.sub(%r{\A#{Regexp.escape ns}:}, "") : key
|
397
457
|
end
|
398
458
|
|
399
459
|
def namespace
|
@@ -423,54 +483,52 @@ module Dalli
|
|
423
483
|
perform do
|
424
484
|
return {} if keys.empty?
|
425
485
|
ring.lock do
|
426
|
-
|
427
|
-
|
428
|
-
|
429
|
-
|
430
|
-
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
|
435
|
-
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
|
443
|
-
|
444
|
-
|
445
|
-
|
446
|
-
|
447
|
-
|
448
|
-
|
449
|
-
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
break
|
486
|
+
groups = groups_for_keys(keys)
|
487
|
+
if (unfound_keys = groups.delete(nil))
|
488
|
+
Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
|
489
|
+
end
|
490
|
+
make_multi_get_requests(groups)
|
491
|
+
|
492
|
+
servers = groups.keys
|
493
|
+
return if servers.empty?
|
494
|
+
servers = perform_multi_response_start(servers)
|
495
|
+
|
496
|
+
start = Time.now
|
497
|
+
loop do
|
498
|
+
# remove any dead servers
|
499
|
+
servers.delete_if { |s| s.sock.nil? }
|
500
|
+
break if servers.empty?
|
501
|
+
|
502
|
+
# calculate remaining timeout
|
503
|
+
elapsed = Time.now - start
|
504
|
+
timeout = servers.first.options[:socket_timeout]
|
505
|
+
time_left = elapsed > timeout ? 0 : timeout - elapsed
|
506
|
+
|
507
|
+
sockets = servers.map(&:sock)
|
508
|
+
readable, _ = IO.select(sockets, nil, nil, time_left)
|
509
|
+
|
510
|
+
if readable.nil?
|
511
|
+
# no response within timeout; abort pending connections
|
512
|
+
servers.each do |server|
|
513
|
+
Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
|
514
|
+
server.multi_response_abort
|
515
|
+
end
|
516
|
+
break
|
458
517
|
|
459
|
-
|
460
|
-
|
461
|
-
|
518
|
+
else
|
519
|
+
readable.each do |sock|
|
520
|
+
server = sock.server
|
462
521
|
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
522
|
+
begin
|
523
|
+
server.multi_response_nonblock.each_pair do |key, value_list|
|
524
|
+
yield key_without_namespace(key), value_list
|
525
|
+
end
|
467
526
|
|
468
|
-
|
469
|
-
servers.delete(server)
|
470
|
-
end
|
471
|
-
rescue NetworkError
|
527
|
+
if server.multi_response_completed?
|
472
528
|
servers.delete(server)
|
473
529
|
end
|
530
|
+
rescue NetworkError
|
531
|
+
servers.delete(server)
|
474
532
|
end
|
475
533
|
end
|
476
534
|
end
|
data/lib/dalli/compressor.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require
|
2
|
+
|
3
|
+
require "zlib"
|
4
|
+
require "stringio"
|
4
5
|
|
5
6
|
module Dalli
|
6
7
|
class Compressor
|
@@ -15,7 +16,7 @@ module Dalli
|
|
15
16
|
|
16
17
|
class GzipCompressor
|
17
18
|
def self.compress(data)
|
18
|
-
io = StringIO.new(
|
19
|
+
io = StringIO.new(+"", "w")
|
19
20
|
gz = Zlib::GzipWriter.new(io)
|
20
21
|
gz.write(data)
|
21
22
|
gz.close
|
data/lib/dalli/options.rb
CHANGED
@@ -1,13 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
require 'thread'
|
3
|
-
require 'monitor'
|
4
2
|
|
5
|
-
|
3
|
+
require "monitor"
|
6
4
|
|
5
|
+
module Dalli
|
7
6
|
# Make Dalli threadsafe by using a lock around all
|
8
7
|
# public server methods.
|
9
8
|
#
|
10
|
-
# Dalli::
|
9
|
+
# Dalli::Protocol::Binary.extend(Dalli::Threadsafe)
|
11
10
|
#
|
12
11
|
module Threadsafe
|
13
12
|
def self.extended(obj)
|