dalli 3.0.4 → 3.0.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 +4 -4
- data/Gemfile +11 -5
- data/History.md +7 -0
- data/README.md +25 -134
- data/lib/dalli/cas/client.rb +2 -0
- data/lib/dalli/client.rb +183 -193
- data/lib/dalli/compressor.rb +13 -4
- data/lib/dalli/key_manager.rb +113 -0
- data/lib/dalli/options.rb +2 -2
- data/lib/dalli/protocol/binary/request_formatter.rb +109 -0
- data/lib/dalli/protocol/binary/response_processor.rb +149 -0
- data/lib/dalli/protocol/binary/sasl_authentication.rb +57 -0
- data/lib/dalli/protocol/binary.rb +271 -430
- data/lib/dalli/protocol/server_config_parser.rb +23 -6
- data/lib/dalli/protocol/value_marshaller.rb +59 -0
- data/lib/dalli/protocol/value_serializer.rb +91 -0
- data/lib/dalli/protocol.rb +2 -3
- data/lib/dalli/ring.rb +91 -35
- data/lib/dalli/server.rb +2 -2
- data/lib/dalli/servers_arg_normalizer.rb +54 -0
- data/lib/dalli/socket.rb +96 -52
- data/lib/dalli/version.rb +3 -1
- data/lib/dalli.rb +31 -14
- data/lib/rack/session/dalli.rb +28 -18
- metadata +70 -6
data/lib/dalli/client.rb
CHANGED
@@ -1,17 +1,24 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'digest/md5'
|
4
|
+
require 'set'
|
5
5
|
|
6
6
|
# encoding: ascii
|
7
7
|
module Dalli
|
8
|
+
##
|
9
|
+
# Dalli::Client is the main class which developers will use to interact with
|
10
|
+
# Memcached.
|
11
|
+
##
|
8
12
|
class Client
|
9
13
|
##
|
10
14
|
# Dalli::Client is the main class which developers will use to interact with
|
11
15
|
# the memcached server. Usage:
|
12
16
|
#
|
13
|
-
# Dalli::Client.new(['localhost:11211:10',
|
14
|
-
#
|
17
|
+
# Dalli::Client.new(['localhost:11211:10',
|
18
|
+
# 'cache-2.example.com:11211:5',
|
19
|
+
# '192.168.0.1:22122:5',
|
20
|
+
# '/var/run/memcached/socket'],
|
21
|
+
# failover: true, expires_in: 300)
|
15
22
|
#
|
16
23
|
# servers is an Array of "host:port:weight" where weight allows you to distribute cache unevenly.
|
17
24
|
# Both weight and port are optional. If you pass in nil, Dalli will use the <tt>MEMCACHE_SERVERS</tt>
|
@@ -24,19 +31,25 @@ module Dalli
|
|
24
31
|
# - :namespace - prepend each key with this value to provide simple namespacing.
|
25
32
|
# - :failover - if a server is down, look for and store values on another server in the ring. Default: true.
|
26
33
|
# - :threadsafe - ensure that only one thread is actively using a socket at a time. Default: true.
|
27
|
-
# - :expires_in - default TTL in seconds if you do not pass TTL as a parameter to an individual operation, defaults
|
28
|
-
#
|
29
|
-
# - :
|
34
|
+
# - :expires_in - default TTL in seconds if you do not pass TTL as a parameter to an individual operation, defaults
|
35
|
+
# to 0 or forever.
|
36
|
+
# - :compress - if true Dalli will compress values larger than compression_min_size bytes before sending them
|
37
|
+
# to memcached. Default: true.
|
38
|
+
# - :compression_min_size - the minimum size (in bytes) for which Dalli will compress values sent to Memcached.
|
39
|
+
# Defaults to 4K.
|
30
40
|
# - :serializer - defaults to Marshal
|
31
|
-
# - :compressor - defaults to
|
32
|
-
# - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for
|
33
|
-
#
|
34
|
-
# - :
|
41
|
+
# - :compressor - defaults to Dalli::Compressor, a Zlib-based implementation
|
42
|
+
# - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for
|
43
|
+
# #fetch operations.
|
44
|
+
# - :digest_class - defaults to Digest::MD5, allows you to pass in an object that responds to the hexdigest method,
|
45
|
+
# useful for injecting a FIPS compliant hash object.
|
46
|
+
# - :protocol_implementation - defaults to Dalli::Protocol::Binary which uses the binary protocol. Allows you to
|
47
|
+
# pass an alternative implementation using another protocol.
|
35
48
|
#
|
36
49
|
def initialize(servers = nil, options = {})
|
37
|
-
|
38
|
-
@servers = normalize_servers(servers || ENV["MEMCACHE_SERVERS"] || "127.0.0.1:11211")
|
50
|
+
@servers = ::Dalli::ServersArgNormalizer.normalize_servers(servers)
|
39
51
|
@options = normalize_options(options)
|
52
|
+
@key_manager = ::Dalli::KeyManager.new(options)
|
40
53
|
@ring = nil
|
41
54
|
end
|
42
55
|
|
@@ -50,9 +63,11 @@ module Dalli
|
|
50
63
|
# pipelined as Dalli will use 'quiet' operations where possible.
|
51
64
|
# Currently supports the set, add, replace and delete operations.
|
52
65
|
def multi
|
53
|
-
old
|
66
|
+
old = Thread.current[:dalli_multi]
|
67
|
+
Thread.current[:dalli_multi] = true
|
54
68
|
yield
|
55
69
|
ensure
|
70
|
+
@ring&.flush_multi_responses
|
56
71
|
Thread.current[:dalli_multi] = old
|
57
72
|
end
|
58
73
|
|
@@ -72,6 +87,7 @@ module Dalli
|
|
72
87
|
keys.compact!
|
73
88
|
|
74
89
|
return {} if keys.empty?
|
90
|
+
|
75
91
|
if block_given?
|
76
92
|
get_multi_yielder(keys) { |k, data| yield k, data.first }
|
77
93
|
else
|
@@ -81,7 +97,7 @@ module Dalli
|
|
81
97
|
end
|
82
98
|
end
|
83
99
|
|
84
|
-
CACHE_NILS = {cache_nils: true}.freeze
|
100
|
+
CACHE_NILS = { cache_nils: true }.freeze
|
85
101
|
|
86
102
|
# Fetch the value associated with the key.
|
87
103
|
# If a value is found, then it is returned.
|
@@ -91,19 +107,24 @@ module Dalli
|
|
91
107
|
# If a value is not found (or if the found value is nil and :cache_nils is false)
|
92
108
|
# and a block is given, the block will be invoked and its return value
|
93
109
|
# written to the cache and returned.
|
94
|
-
def fetch(key, ttl = nil,
|
95
|
-
|
96
|
-
val = get(key,
|
97
|
-
not_found
|
98
|
-
val == Dalli::Protocol::NOT_FOUND :
|
99
|
-
val.nil?
|
100
|
-
if not_found && block_given?
|
110
|
+
def fetch(key, ttl = nil, req_options = nil)
|
111
|
+
req_options = req_options.nil? ? CACHE_NILS : req_options.merge(CACHE_NILS) if cache_nils
|
112
|
+
val = get(key, req_options)
|
113
|
+
if not_found?(val) && block_given?
|
101
114
|
val = yield
|
102
|
-
add(key, val, ttl_or_default(ttl),
|
115
|
+
add(key, val, ttl_or_default(ttl), req_options)
|
103
116
|
end
|
104
117
|
val
|
105
118
|
end
|
106
119
|
|
120
|
+
def not_found?(val)
|
121
|
+
cache_nils ? val == ::Dalli::NOT_FOUND : val.nil?
|
122
|
+
end
|
123
|
+
|
124
|
+
def cache_nils
|
125
|
+
@options[:cache_nils]
|
126
|
+
end
|
127
|
+
|
107
128
|
##
|
108
129
|
# compare and swap values using optimistic locking.
|
109
130
|
# Fetch the existing value for key.
|
@@ -171,7 +192,7 @@ module Dalli
|
|
171
192
|
ring.servers.map { |s| s.request(:flush, time += delay) }
|
172
193
|
end
|
173
194
|
|
174
|
-
|
195
|
+
alias flush_all flush
|
175
196
|
|
176
197
|
##
|
177
198
|
# Incr adds the given amount to the counter on the memcached server.
|
@@ -185,7 +206,8 @@ module Dalli
|
|
185
206
|
# exist. To increase an existing counter and update its TTL, use
|
186
207
|
# #cas.
|
187
208
|
def incr(key, amt = 1, ttl = nil, default = nil)
|
188
|
-
raise ArgumentError, "Positive values only: #{amt}" if amt
|
209
|
+
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
|
210
|
+
|
189
211
|
perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
|
190
212
|
end
|
191
213
|
|
@@ -204,7 +226,8 @@ module Dalli
|
|
204
226
|
# exist. To decrease an existing counter and update its TTL, use
|
205
227
|
# #cas.
|
206
228
|
def decr(key, amt = 1, ttl = nil, default = nil)
|
207
|
-
raise ArgumentError, "Positive values only: #{amt}" if amt
|
229
|
+
raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
|
230
|
+
|
208
231
|
perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
|
209
232
|
end
|
210
233
|
|
@@ -249,7 +272,7 @@ module Dalli
|
|
249
272
|
##
|
250
273
|
## Make sure memcache servers are alive, or raise an Dalli::RingError
|
251
274
|
def alive!
|
252
|
-
ring.server_for_key(
|
275
|
+
ring.server_for_key('')
|
253
276
|
end
|
254
277
|
|
255
278
|
##
|
@@ -267,7 +290,7 @@ module Dalli
|
|
267
290
|
# value and CAS will be passed to the block.
|
268
291
|
def get_cas(key)
|
269
292
|
(value, cas) = perform(:cas, key)
|
270
|
-
value = !value || value ==
|
293
|
+
value = nil if !value || value == 'Not found'
|
271
294
|
if block_given?
|
272
295
|
yield value, cas
|
273
296
|
else
|
@@ -318,12 +341,12 @@ module Dalli
|
|
318
341
|
# Close our connection to each server.
|
319
342
|
# If you perform another operation after this, the connections will be re-established.
|
320
343
|
def close
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
344
|
+
return unless @ring
|
345
|
+
|
346
|
+
@ring.servers.each(&:close)
|
347
|
+
@ring = nil
|
325
348
|
end
|
326
|
-
|
349
|
+
alias reset close
|
327
350
|
|
328
351
|
# Stub method so a bare Dalli client can pretend to be a connection pool.
|
329
352
|
def with
|
@@ -334,8 +357,9 @@ module Dalli
|
|
334
357
|
|
335
358
|
def cas_core(key, always_set, ttl = nil, options = nil)
|
336
359
|
(value, cas) = perform(:cas, key)
|
337
|
-
value = !value || value ==
|
360
|
+
value = nil if !value || value == 'Not found'
|
338
361
|
return if value.nil? && !always_set
|
362
|
+
|
339
363
|
newvalue = yield(value)
|
340
364
|
perform(:set, key, newvalue, ttl_or_default(ttl), cas, options)
|
341
365
|
end
|
@@ -346,26 +370,90 @@ module Dalli
|
|
346
370
|
raise ArgumentError, "Cannot convert ttl (#{ttl}) to an integer"
|
347
371
|
end
|
348
372
|
|
349
|
-
def
|
350
|
-
|
351
|
-
|
373
|
+
def ring
|
374
|
+
# TODO: This server initialization should probably be pushed down
|
375
|
+
# to the Ring
|
376
|
+
@ring ||= Dalli::Ring.new(
|
377
|
+
@servers.map do |s|
|
378
|
+
protocol_implementation.new(s, @options)
|
379
|
+
end, @options
|
380
|
+
)
|
381
|
+
end
|
352
382
|
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
383
|
+
def protocol_implementation
|
384
|
+
@protocol_implementation ||= @options.fetch(:protocol_implementation, Dalli::Protocol::Binary)
|
385
|
+
end
|
386
|
+
|
387
|
+
# Chokepoint method for instrumentation
|
388
|
+
def perform(*all_args)
|
389
|
+
return yield if block_given?
|
390
|
+
|
391
|
+
op, key, *args = all_args
|
392
|
+
|
393
|
+
key = key.to_s
|
394
|
+
key = @key_manager.validate_key(key)
|
395
|
+
|
396
|
+
server = ring.server_for_key(key)
|
397
|
+
server.request(op, key, *args)
|
398
|
+
rescue NetworkError => e
|
399
|
+
Dalli.logger.debug { e.inspect }
|
400
|
+
Dalli.logger.debug { 'retrying request with new server' }
|
401
|
+
retry
|
402
|
+
end
|
403
|
+
|
404
|
+
def normalize_options(opts)
|
405
|
+
begin
|
406
|
+
opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in]
|
407
|
+
rescue NoMethodError
|
408
|
+
raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
|
409
|
+
end
|
410
|
+
opts
|
411
|
+
end
|
412
|
+
|
413
|
+
# TODO: Look at extracting below into separate MultiYielder class
|
414
|
+
|
415
|
+
##
|
416
|
+
# Yields, one at a time, keys and their values+attributes.
|
417
|
+
#
|
418
|
+
def get_multi_yielder(keys, &block)
|
419
|
+
return {} if keys.empty?
|
420
|
+
|
421
|
+
ring.lock do
|
422
|
+
groups = groups_for_keys(keys)
|
423
|
+
if (unfound_keys = groups.delete(nil))
|
424
|
+
Dalli.logger.debug do
|
425
|
+
"unable to get keys for #{unfound_keys.length} keys "\
|
426
|
+
'because no matching server was found'
|
427
|
+
end
|
359
428
|
end
|
360
|
-
|
429
|
+
make_multi_get_requests(groups)
|
430
|
+
|
431
|
+
servers = groups.keys
|
432
|
+
return if servers.empty?
|
433
|
+
|
434
|
+
# TODO: How does this exit on a NetworkError
|
435
|
+
servers = perform_multi_response_start(servers)
|
436
|
+
|
437
|
+
timeout = servers.first.options[:socket_timeout]
|
438
|
+
start_time = Time.now
|
439
|
+
loop do
|
440
|
+
# remove any dead servers
|
441
|
+
# TODO: Is this well behaved in a multi-threaded environment?
|
442
|
+
# Accessing the server socket like this seems problematic
|
443
|
+
servers.delete_if { |s| s.sock.nil? }
|
444
|
+
break if servers.empty?
|
445
|
+
|
446
|
+
servers = multi_yielder_loop(servers, start_time, timeout, &block)
|
447
|
+
end
|
448
|
+
end
|
449
|
+
rescue NetworkError => e
|
450
|
+
Dalli.logger.debug { e.inspect }
|
451
|
+
Dalli.logger.debug { 'retrying multi yielder because of timeout' }
|
452
|
+
retry
|
361
453
|
end
|
362
454
|
|
363
455
|
def make_multi_get_requests(groups)
|
364
456
|
groups.each do |server, keys_for_server|
|
365
|
-
# TODO: do this with the perform chokepoint?
|
366
|
-
# But given the fact that fetching the response doesn't take place
|
367
|
-
# in that slot it's misleading anyway. Need to move all of this method
|
368
|
-
# into perform to be meaningful
|
369
457
|
server.request(:send_multiget, keys_for_server)
|
370
458
|
rescue DalliError, NetworkError => e
|
371
459
|
Dalli.logger.debug { e.inspect }
|
@@ -373,184 +461,86 @@ module Dalli
|
|
373
461
|
end
|
374
462
|
end
|
375
463
|
|
464
|
+
# raises Dalli::NetworkError
|
376
465
|
def perform_multi_response_start(servers)
|
377
466
|
deleted = []
|
378
|
-
|
467
|
+
|
379
468
|
servers.each do |server|
|
380
469
|
next unless server.alive?
|
381
|
-
|
470
|
+
|
382
471
|
begin
|
383
472
|
server.multi_response_start
|
384
473
|
rescue Dalli::NetworkError
|
385
|
-
servers
|
474
|
+
abort_multi_response(servers)
|
386
475
|
raise
|
387
476
|
rescue Dalli::DalliError => e
|
388
477
|
Dalli.logger.debug { e.inspect }
|
389
|
-
Dalli.logger.debug {
|
478
|
+
Dalli.logger.debug { 'results from this server will be missing' }
|
390
479
|
deleted.append(server)
|
391
480
|
end
|
392
481
|
end
|
393
|
-
|
482
|
+
|
394
483
|
servers.delete_if { |server| deleted.include?(server) }
|
395
484
|
end
|
396
485
|
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
return if servers.nil?
|
401
|
-
return if servers.is_a?(Array)
|
402
|
-
return if servers.is_a?(String)
|
403
|
-
|
404
|
-
raise ArgumentError, "An explicit servers argument must be a comma separated string or an array containing strings."
|
486
|
+
# Swallows Dalli::NetworkError
|
487
|
+
def abort_multi_response(servers)
|
488
|
+
servers.each(&:multi_response_abort)
|
405
489
|
end
|
406
490
|
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
411
|
-
|
412
|
-
|
413
|
-
if server.is_a? String
|
414
|
-
server.split(",")
|
415
|
-
else
|
416
|
-
server
|
417
|
-
end
|
491
|
+
def multi_yielder_loop(servers, start_time, timeout, &block)
|
492
|
+
time_left = remaining_time(start_time, timeout)
|
493
|
+
readable_servers = servers_with_data(servers, time_left)
|
494
|
+
if readable_servers.empty?
|
495
|
+
abort_multi_connections_w_timeout(servers)
|
496
|
+
return readable_servers
|
418
497
|
end
|
419
|
-
end
|
420
498
|
|
421
|
-
|
422
|
-
|
423
|
-
@servers.map { |s|
|
424
|
-
server_options = {}
|
425
|
-
if s.start_with?("memcached://")
|
426
|
-
uri = URI.parse(s)
|
427
|
-
server_options[:username] = uri.user
|
428
|
-
server_options[:password] = uri.password
|
429
|
-
s = "#{uri.host}:#{uri.port}"
|
430
|
-
end
|
431
|
-
@options.fetch(:protocol_implementation, Dalli::Protocol::Binary).new(s, @options.merge(server_options))
|
432
|
-
}, @options
|
433
|
-
)
|
434
|
-
end
|
435
|
-
|
436
|
-
# Chokepoint method for instrumentation
|
437
|
-
def perform(*all_args)
|
438
|
-
begin
|
439
|
-
return yield if block_given?
|
440
|
-
op, key, *args = all_args
|
441
|
-
|
442
|
-
key = key.to_s
|
443
|
-
key = validate_key(key)
|
444
|
-
|
445
|
-
server = ring.server_for_key(key)
|
446
|
-
server.request(op, key, *args)
|
447
|
-
rescue NetworkError => e
|
448
|
-
Dalli.logger.debug { e.inspect }
|
449
|
-
Dalli.logger.debug { "retrying request with new server" }
|
450
|
-
retry
|
499
|
+
readable_servers.each do |server|
|
500
|
+
servers.delete(server) if respond_to_readable_server(server, &block)
|
451
501
|
end
|
502
|
+
servers
|
503
|
+
rescue NetworkError
|
504
|
+
abort_multi_response(servers)
|
505
|
+
raise
|
452
506
|
end
|
453
507
|
|
454
|
-
def
|
455
|
-
|
456
|
-
|
457
|
-
if key.length > 250
|
458
|
-
digest_class = @options[:digest_class] || ::Digest::MD5
|
459
|
-
max_length_before_namespace = 212 - (namespace || "").size
|
460
|
-
key = "#{key[0, max_length_before_namespace]}:md5:#{digest_class.hexdigest(key)}"
|
461
|
-
end
|
462
|
-
key
|
463
|
-
end
|
508
|
+
def remaining_time(start, timeout)
|
509
|
+
elapsed = Time.now - start
|
510
|
+
return 0 if elapsed > timeout
|
464
511
|
|
465
|
-
|
466
|
-
(ns = namespace) ? "#{ns}:#{key}" : key
|
512
|
+
timeout - elapsed
|
467
513
|
end
|
468
514
|
|
469
|
-
|
470
|
-
|
471
|
-
|
515
|
+
# Swallows Dalli::NetworkError
|
516
|
+
def abort_multi_connections_w_timeout(servers)
|
517
|
+
abort_multi_response(servers)
|
518
|
+
servers.each do |server|
|
519
|
+
Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
|
520
|
+
end
|
472
521
|
|
473
|
-
|
474
|
-
return nil unless @options[:namespace]
|
475
|
-
@options[:namespace].is_a?(Proc) ? @options[:namespace].call.to_s : @options[:namespace].to_s
|
522
|
+
true # Required to simplify caller
|
476
523
|
end
|
477
524
|
|
478
|
-
def
|
479
|
-
|
480
|
-
|
481
|
-
opts[:compress] = opts.delete(:compression)
|
525
|
+
def respond_to_readable_server(server)
|
526
|
+
server.multi_response_nonblock.each_pair do |key, value_list|
|
527
|
+
yield @key_manager.key_without_namespace(key), value_list
|
482
528
|
end
|
483
|
-
|
484
|
-
|
485
|
-
rescue NoMethodError
|
486
|
-
raise ArgumentError, "cannot convert :expires_in => #{opts[:expires_in].inspect} to an integer"
|
487
|
-
end
|
488
|
-
if opts[:digest_class] && !opts[:digest_class].respond_to?(:hexdigest)
|
489
|
-
raise ArgumentError, "The digest_class object must respond to the hexdigest method"
|
490
|
-
end
|
491
|
-
opts
|
529
|
+
|
530
|
+
server.multi_response_completed?
|
492
531
|
end
|
493
532
|
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
ring.lock do
|
500
|
-
groups = groups_for_keys(keys)
|
501
|
-
if (unfound_keys = groups.delete(nil))
|
502
|
-
Dalli.logger.debug { "unable to get keys for #{unfound_keys.length} keys because no matching server was found" }
|
503
|
-
end
|
504
|
-
make_multi_get_requests(groups)
|
505
|
-
|
506
|
-
servers = groups.keys
|
507
|
-
return if servers.empty?
|
508
|
-
servers = perform_multi_response_start(servers)
|
509
|
-
|
510
|
-
start = Time.now
|
511
|
-
loop do
|
512
|
-
# remove any dead servers
|
513
|
-
servers.delete_if { |s| s.sock.nil? }
|
514
|
-
break if servers.empty?
|
515
|
-
|
516
|
-
# calculate remaining timeout
|
517
|
-
elapsed = Time.now - start
|
518
|
-
timeout = servers.first.options[:socket_timeout]
|
519
|
-
time_left = elapsed > timeout ? 0 : timeout - elapsed
|
520
|
-
|
521
|
-
sockets = servers.map(&:sock)
|
522
|
-
readable, _ = IO.select(sockets, nil, nil, time_left)
|
523
|
-
|
524
|
-
if readable.nil?
|
525
|
-
# no response within timeout; abort pending connections
|
526
|
-
servers.each do |server|
|
527
|
-
Dalli.logger.debug { "memcached at #{server.name} did not response within timeout" }
|
528
|
-
server.multi_response_abort
|
529
|
-
end
|
530
|
-
break
|
531
|
-
|
532
|
-
else
|
533
|
-
readable.each do |sock|
|
534
|
-
server = sock.server
|
535
|
-
|
536
|
-
begin
|
537
|
-
server.multi_response_nonblock.each_pair do |key, value_list|
|
538
|
-
yield key_without_namespace(key), value_list
|
539
|
-
end
|
540
|
-
|
541
|
-
if server.multi_response_completed?
|
542
|
-
servers.delete(server)
|
543
|
-
end
|
544
|
-
rescue NetworkError
|
545
|
-
servers.each { |s| s.multi_response_abort unless s.sock.nil? }
|
546
|
-
raise
|
547
|
-
end
|
548
|
-
end
|
549
|
-
end
|
550
|
-
end
|
551
|
-
end
|
552
|
-
end
|
533
|
+
def servers_with_data(servers, timeout)
|
534
|
+
readable, = IO.select(servers.map(&:sock), nil, nil, timeout)
|
535
|
+
return [] if readable.nil?
|
536
|
+
|
537
|
+
readable.map(&:server)
|
553
538
|
end
|
554
539
|
|
540
|
+
def groups_for_keys(*keys)
|
541
|
+
keys.flatten!
|
542
|
+
keys.map! { |a| @key_manager.validate_key(a.to_s) }
|
543
|
+
ring.keys_grouped_by_server(keys)
|
544
|
+
end
|
555
545
|
end
|
556
546
|
end
|
data/lib/dalli/compressor.rb
CHANGED
@@ -1,9 +1,13 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require 'zlib'
|
4
|
+
require 'stringio'
|
5
5
|
|
6
6
|
module Dalli
|
7
|
+
##
|
8
|
+
# Default compressor used by Dalli, that uses
|
9
|
+
# Zlib DEFLATE to compress data.
|
10
|
+
##
|
7
11
|
class Compressor
|
8
12
|
def self.compress(data)
|
9
13
|
Zlib::Deflate.deflate(data)
|
@@ -14,9 +18,14 @@ module Dalli
|
|
14
18
|
end
|
15
19
|
end
|
16
20
|
|
21
|
+
##
|
22
|
+
# Alternate compressor for Dalli, that uses
|
23
|
+
# Gzip. Gzip adds a checksum to each compressed
|
24
|
+
# entry.
|
25
|
+
##
|
17
26
|
class GzipCompressor
|
18
27
|
def self.compress(data)
|
19
|
-
io = StringIO.new(+
|
28
|
+
io = StringIO.new(+'', 'w')
|
20
29
|
gz = Zlib::GzipWriter.new(io)
|
21
30
|
gz.write(data)
|
22
31
|
gz.close
|
@@ -24,7 +33,7 @@ module Dalli
|
|
24
33
|
end
|
25
34
|
|
26
35
|
def self.decompress(data)
|
27
|
-
io = StringIO.new(data,
|
36
|
+
io = StringIO.new(data, 'rb')
|
28
37
|
Zlib::GzipReader.new(io).read
|
29
38
|
end
|
30
39
|
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'digest/md5'
|
4
|
+
|
5
|
+
module Dalli
|
6
|
+
##
|
7
|
+
# This class manages and validates keys sent to Memcached, ensuring
|
8
|
+
# that they meet Memcached key length requirements, and supporting
|
9
|
+
# the implementation of optional namespaces on a per-Dalli client
|
10
|
+
# basis.
|
11
|
+
##
|
12
|
+
class KeyManager
|
13
|
+
MAX_KEY_LENGTH = 250
|
14
|
+
|
15
|
+
NAMESPACE_SEPARATOR = ':'
|
16
|
+
|
17
|
+
# This is a hard coded md5 for historical reasons
|
18
|
+
TRUNCATED_KEY_SEPARATOR = ':md5:'
|
19
|
+
|
20
|
+
# This is 249 for historical reasons
|
21
|
+
TRUNCATED_KEY_TARGET_SIZE = 249
|
22
|
+
|
23
|
+
DEFAULTS = {
|
24
|
+
digest_class: ::Digest::MD5
|
25
|
+
}.freeze
|
26
|
+
|
27
|
+
OPTIONS = %i[digest_class namespace].freeze
|
28
|
+
|
29
|
+
attr_reader :namespace
|
30
|
+
|
31
|
+
def initialize(client_options)
|
32
|
+
@key_options =
|
33
|
+
DEFAULTS.merge(client_options.select { |k, _| OPTIONS.include?(k) })
|
34
|
+
validate_digest_class_option(@key_options)
|
35
|
+
|
36
|
+
@namespace = namespace_from_options
|
37
|
+
end
|
38
|
+
|
39
|
+
##
|
40
|
+
# Validates the key, and transforms as needed.
|
41
|
+
#
|
42
|
+
# If the key is nil or empty, raises ArgumentError. Whitespace
|
43
|
+
# characters are allowed for historical reasons, but likely shouldn't
|
44
|
+
# be used.
|
45
|
+
# If the key (with namespace) is shorter than the memcached maximum
|
46
|
+
# allowed key length, just returns the argument key
|
47
|
+
# Otherwise computes a "truncated" key that uses a truncated prefix
|
48
|
+
# combined with a 32-byte hex digest of the whole key.
|
49
|
+
##
|
50
|
+
def validate_key(key)
|
51
|
+
raise ArgumentError, 'key cannot be blank' unless key&.length&.positive?
|
52
|
+
|
53
|
+
key = key_with_namespace(key)
|
54
|
+
key.length > MAX_KEY_LENGTH ? truncated_key(key) : key
|
55
|
+
end
|
56
|
+
|
57
|
+
##
|
58
|
+
# Returns the key with the namespace prefixed, if a namespace is
|
59
|
+
# defined. Otherwise just returns the key
|
60
|
+
##
|
61
|
+
def key_with_namespace(key)
|
62
|
+
return key if namespace.nil?
|
63
|
+
|
64
|
+
"#{namespace}#{NAMESPACE_SEPARATOR}#{key}"
|
65
|
+
end
|
66
|
+
|
67
|
+
def key_without_namespace(key)
|
68
|
+
return key if namespace.nil?
|
69
|
+
|
70
|
+
key.sub(namespace_regexp, '')
|
71
|
+
end
|
72
|
+
|
73
|
+
def digest_class
|
74
|
+
@digest_class ||= @key_options[:digest_class]
|
75
|
+
end
|
76
|
+
|
77
|
+
def namespace_regexp
|
78
|
+
@namespace_regexp ||= /\A#{Regexp.escape(namespace)}:/.freeze unless namespace.nil?
|
79
|
+
end
|
80
|
+
|
81
|
+
def validate_digest_class_option(opts)
|
82
|
+
return if opts[:digest_class].respond_to?(:hexdigest)
|
83
|
+
|
84
|
+
raise ArgumentError, 'The digest_class object must respond to the hexdigest method'
|
85
|
+
end
|
86
|
+
|
87
|
+
def namespace_from_options
|
88
|
+
raw_namespace = @key_options[:namespace]
|
89
|
+
return nil unless raw_namespace
|
90
|
+
return raw_namespace.call.to_s if raw_namespace.is_a?(Proc)
|
91
|
+
|
92
|
+
raw_namespace.to_s
|
93
|
+
end
|
94
|
+
|
95
|
+
##
|
96
|
+
# Produces a truncated key, if the raw key is longer than the maximum allowed
|
97
|
+
# length. The truncated key is produced by generating a hex digest
|
98
|
+
# of the key, and appending that to a truncated section of the key.
|
99
|
+
##
|
100
|
+
def truncated_key(key)
|
101
|
+
digest = digest_class.hexdigest(key)
|
102
|
+
"#{key[0, prefix_length(digest)]}#{TRUNCATED_KEY_SEPARATOR}#{digest}"
|
103
|
+
end
|
104
|
+
|
105
|
+
def prefix_length(digest)
|
106
|
+
return TRUNCATED_KEY_TARGET_SIZE - (TRUNCATED_KEY_SEPARATOR.length + digest.length) if namespace.nil?
|
107
|
+
|
108
|
+
# For historical reasons, truncated keys with namespaces had a length of 250 rather
|
109
|
+
# than 249
|
110
|
+
TRUNCATED_KEY_TARGET_SIZE + 1 - (TRUNCATED_KEY_SEPARATOR.length + digest.length)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|