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.

data/lib/dalli/client.rb CHANGED
@@ -1,17 +1,24 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "digest/md5"
4
- require "set"
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', 'cache-2.example.com:11211:5', '192.168.0.1:22122:5', '/var/run/memcached/socket'],
14
- # :threadsafe => true, :failover => true, :expires_in => 300)
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 to 0 or forever
28
- # - :compress - if true Dalli will compress values larger than compression_min_size bytes before sending them to memcached. Default: true.
29
- # - :compression_min_size - the minimum size (in bytes) for which Dalli will compress values sent to Memcached. Defaults to 4K.
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 zlib
32
- # - :cache_nils - defaults to false, if true Dalli will not treat cached nil values as 'not found' for #fetch operations.
33
- # - :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.
34
- # - :protocol_implementation - defaults to Dalli::Protocol::Binary which uses the binary protocol. Allows you to pass an alternative implementation using another protocol.
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
- validate_servers_arg(servers)
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, Thread.current[:dalli_multi] = Thread.current[:dalli_multi], true
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, options = nil)
95
- options = options.nil? ? CACHE_NILS : options.merge(CACHE_NILS) if @options[:cache_nils]
96
- val = get(key, options)
97
- not_found = @options[:cache_nils] ?
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), options)
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
- alias_method :flush_all, :flush
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 < 0
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 < 0
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 == "Not found" ? nil : 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
- if @ring
322
- @ring.servers.each { |s| s.close }
323
- @ring = nil
324
- end
344
+ return unless @ring
345
+
346
+ @ring.servers.each(&:close)
347
+ @ring = nil
325
348
  end
326
- alias_method :reset, :close
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 == "Not found" ? nil : 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 groups_for_keys(*keys)
350
- keys.flatten!
351
- keys.map! { |a| validate_key(a.to_s) }
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
- keys.group_by { |key|
354
- begin
355
- ring.server_for_key(key)
356
- rescue Dalli::RingError
357
- Dalli.logger.debug { "unable to get key #{key}" }
358
- nil
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.each { |s| s.multi_response_abort unless s.sock.nil? }
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 { "results from this server will be missing" }
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
- # Ensures that the servers arg is either an array or a string.
399
- def validate_servers_arg(servers)
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
- # Normalizes the argument into an array of servers.
409
- # If the argument is a string, or an array containing strings, it's expected that the URIs are comma separated e.g.
410
- # "memcache1.example.com:11211,memcache2.example.com:11211,memcache3.example.com:11211"
411
- def normalize_servers(servers)
412
- Array(servers).flat_map do |server|
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
- def ring
422
- @ring ||= Dalli::Ring.new(
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 validate_key(key)
455
- raise ArgumentError, "key cannot be blank" if !key || key.length == 0
456
- key = key_with_namespace(key)
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
- def key_with_namespace(key)
466
- (ns = namespace) ? "#{ns}:#{key}" : key
512
+ timeout - elapsed
467
513
  end
468
514
 
469
- def key_without_namespace(key)
470
- (ns = namespace) ? key.sub(%r{\A#{Regexp.escape ns}:}, "") : key
471
- end
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
- def namespace
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 normalize_options(opts)
479
- if opts[:compression]
480
- Dalli.logger.warn "DEPRECATED: Dalli's :compression option is now just :compress => true. Please update your configuration."
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
- begin
484
- opts[:expires_in] = opts[:expires_in].to_i if opts[:expires_in]
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
- # Yields, one at a time, keys and their values+attributes.
496
- def get_multi_yielder(keys)
497
- perform do
498
- return {} if keys.empty?
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
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "zlib"
4
- require "stringio"
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(+"", "w")
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, "rb")
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