dalli 2.7.0 → 2.7.11

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/server.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require 'socket'
2
3
  require 'timeout'
3
4
 
@@ -8,10 +9,13 @@ module Dalli
8
9
  attr_accessor :weight
9
10
  attr_accessor :options
10
11
  attr_reader :sock
12
+ attr_reader :socket_type # possible values: :unix, :tcp
11
13
 
14
+ DEFAULT_PORT = 11211
15
+ DEFAULT_WEIGHT = 1
12
16
  DEFAULTS = {
13
17
  # seconds between trying to contact a remote server
14
- :down_retry_delay => 1,
18
+ :down_retry_delay => 60,
15
19
  # connect/read/write timeout for socket operations
16
20
  :socket_timeout => 0.5,
17
21
  # times a socket operation may fail before considering the server dead
@@ -20,6 +24,8 @@ module Dalli
20
24
  :socket_failure_delay => 0.01,
21
25
  # max size of value in bytes (default is 1 MB, can be overriden with "memcached -I <size>")
22
26
  :value_max_bytes => 1024 * 1024,
27
+ # surpassing value_max_bytes either warns (false) or throws (true)
28
+ :error_when_over_max_size => false,
23
29
  :compressor => Compressor,
24
30
  # min byte size to attempt compression
25
31
  :compression_min_size => 1024,
@@ -28,15 +34,15 @@ module Dalli
28
34
  :serializer => Marshal,
29
35
  :username => nil,
30
36
  :password => nil,
31
- :keepalive => true
37
+ :keepalive => true,
38
+ # max byte size for SO_SNDBUF
39
+ :sndbuf => nil,
40
+ # max byte size for SO_RCVBUF
41
+ :rcvbuf => nil
32
42
  }
33
43
 
34
44
  def initialize(attribs, options = {})
35
- (@hostname, @port, @weight) = attribs.split(':')
36
- @port ||= 11211
37
- @port = Integer(@port)
38
- @weight ||= 1
39
- @weight = Integer(@weight)
45
+ @hostname, @port, @weight, @socket_type = parse_hostname(attribs)
40
46
  @fail_count = 0
41
47
  @down_at = nil
42
48
  @last_down_at = nil
@@ -49,27 +55,28 @@ module Dalli
49
55
  end
50
56
 
51
57
  def name
52
- "#{@hostname}:#{@port}"
58
+ if socket_type == :unix
59
+ hostname
60
+ else
61
+ "#{hostname}:#{port}"
62
+ end
53
63
  end
54
64
 
55
65
  # Chokepoint method for instrumentation
56
66
  def request(op, *args)
57
67
  verify_state
58
- raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}. If you are sure it is running, ensure memcached version is > 1.4." unless alive?
68
+ raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}. If you are sure it is running, ensure memcached version is > 1.4." unless alive?
59
69
  begin
60
70
  send(op, *args)
61
- rescue Dalli::NetworkError
62
- raise
63
71
  rescue Dalli::MarshalError => ex
64
72
  Dalli.logger.error "Marshalling error for key '#{args.first}': #{ex.message}"
65
73
  Dalli.logger.error "You are trying to cache a Ruby object which cannot be serialized to memcached."
66
74
  Dalli.logger.error ex.backtrace.join("\n\t")
67
75
  false
68
- rescue Dalli::DalliError
76
+ rescue Dalli::DalliError, Dalli::NetworkError, Dalli::ValueOverMaxSize, Timeout::Error
69
77
  raise
70
78
  rescue => ex
71
- Dalli.logger.error "Unexpected exception in Dalli: #{ex.class.name}: #{ex.message}"
72
- Dalli.logger.error "This is a bug in Dalli, please enter an issue in Github if it does not already exist."
79
+ Dalli.logger.error "Unexpected exception during Dalli request: #{ex.class.name}: #{ex.message}"
73
80
  Dalli.logger.error ex.backtrace.join("\n\t")
74
81
  down!
75
82
  end
@@ -80,7 +87,7 @@ module Dalli
80
87
 
81
88
  if @last_down_at && @last_down_at + options[:down_retry_delay] >= Time.now
82
89
  time = @last_down_at + options[:down_retry_delay] - Time.now
83
- Dalli.logger.debug { "down_retry_delay not reached for #{hostname}:#{port} (%.3f seconds left)" % time }
90
+ Dalli.logger.debug { "down_retry_delay not reached for #{name} (%.3f seconds left)" % time }
84
91
  return false
85
92
  end
86
93
 
@@ -120,7 +127,7 @@ module Dalli
120
127
  def multi_response_start
121
128
  verify_state
122
129
  write_noop
123
- @multi_buffer = ''
130
+ @multi_buffer = String.new('')
124
131
  @position = 0
125
132
  @inprogress = true
126
133
  end
@@ -199,20 +206,28 @@ module Dalli
199
206
 
200
207
  def verify_state
201
208
  failure!(RuntimeError.new('Already writing to socket')) if @inprogress
202
- failure!(RuntimeError.new('Cannot share client between multiple processes')) if @pid && @pid != Process.pid
209
+ if @pid && @pid != Process.pid
210
+ message = 'Fork detected, re-connecting child process...'
211
+ Dalli.logger.info { message }
212
+ reconnect! message
213
+ end
214
+ end
215
+
216
+ def reconnect!(message)
217
+ close
218
+ sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
219
+ raise Dalli::NetworkError, message
203
220
  end
204
221
 
205
222
  def failure!(exception)
206
- message = "#{hostname}:#{port} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
207
- Dalli.logger.info { message }
223
+ message = "#{name} failed (count: #{@fail_count}) #{exception.class}: #{exception.message}"
224
+ Dalli.logger.warn { message }
208
225
 
209
226
  @fail_count += 1
210
227
  if @fail_count >= options[:socket_max_failures]
211
228
  down!
212
229
  else
213
- close
214
- sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
215
- raise Dalli::NetworkError, "Socket operation failed, retrying..."
230
+ reconnect! 'Socket operation failed, retrying...'
216
231
  end
217
232
  end
218
233
 
@@ -223,21 +238,21 @@ module Dalli
223
238
 
224
239
  if @down_at
225
240
  time = Time.now - @down_at
226
- Dalli.logger.debug { "#{hostname}:#{port} is still down (for %.3f seconds now)" % time }
241
+ Dalli.logger.debug { "#{name} is still down (for %.3f seconds now)" % time }
227
242
  else
228
243
  @down_at = @last_down_at
229
- Dalli.logger.warn { "#{hostname}:#{port} is down" }
244
+ Dalli.logger.warn { "#{name} is down" }
230
245
  end
231
246
 
232
247
  @error = $! && $!.class.name
233
248
  @msg = @msg || ($! && $!.message && !$!.message.empty? && $!.message)
234
- raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}"
249
+ raise Dalli::NetworkError, "#{name} is down: #{@error} #{@msg}"
235
250
  end
236
251
 
237
252
  def up!
238
253
  if @down_at
239
254
  time = Time.now - @down_at
240
- Dalli.logger.warn { "#{hostname}:#{port} is back (downtime was %.3f seconds)" % time }
255
+ Dalli.logger.warn { "#{name} is back (downtime was %.3f seconds)" % time }
241
256
  end
242
257
 
243
258
  @fail_count = 0
@@ -251,14 +266,14 @@ module Dalli
251
266
  Thread.current[:dalli_multi]
252
267
  end
253
268
 
254
- def get(key)
269
+ def get(key, options=nil)
255
270
  req = [REQUEST, OPCODES[:get], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:get])
256
271
  write(req)
257
- generic_response(true)
272
+ generic_response(true, !!(options && options.is_a?(Hash) && options[:cache_nils]))
258
273
  end
259
274
 
260
275
  def send_multiget(keys)
261
- req = ""
276
+ req = String.new("")
262
277
  keys.each do |key|
263
278
  req << [REQUEST, OPCODES[:getkq], key.bytesize, 0, 0, 0, key.bytesize, 0, 0, key].pack(FORMAT[:getkq])
264
279
  end
@@ -268,6 +283,7 @@ module Dalli
268
283
 
269
284
  def set(key, value, ttl, cas, options)
270
285
  (value, flags) = serialize(key, value, options)
286
+ ttl = sanitize_ttl(ttl)
271
287
 
272
288
  guard_max_value(key, value) do
273
289
  req = [REQUEST, OPCODES[multi? ? :setq : :set], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:set])
@@ -278,6 +294,7 @@ module Dalli
278
294
 
279
295
  def add(key, value, ttl, options)
280
296
  (value, flags) = serialize(key, value, options)
297
+ ttl = sanitize_ttl(ttl)
281
298
 
282
299
  guard_max_value(key, value) do
283
300
  req = [REQUEST, OPCODES[multi? ? :addq : :add], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, 0, flags, ttl, key, value].pack(FORMAT[:add])
@@ -288,6 +305,7 @@ module Dalli
288
305
 
289
306
  def replace(key, value, ttl, cas, options)
290
307
  (value, flags) = serialize(key, value, options)
308
+ ttl = sanitize_ttl(ttl)
291
309
 
292
310
  guard_max_value(key, value) do
293
311
  req = [REQUEST, OPCODES[multi? ? :replaceq : :replace], key.bytesize, 8, 0, 0, value.bytesize + key.bytesize + 8, 0, cas, flags, ttl, key, value].pack(FORMAT[:replace])
@@ -308,26 +326,32 @@ module Dalli
308
326
  generic_response
309
327
  end
310
328
 
311
- def decr(key, count, ttl, default)
312
- expiry = default ? ttl : 0xFFFFFFFF
329
+ def decr_incr(opcode, key, count, ttl, default)
330
+ expiry = default ? sanitize_ttl(ttl) : 0xFFFFFFFF
313
331
  default ||= 0
314
332
  (h, l) = split(count)
315
333
  (dh, dl) = split(default)
316
- req = [REQUEST, OPCODES[:decr], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:decr])
334
+ req = [REQUEST, OPCODES[opcode], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[opcode])
317
335
  write(req)
318
336
  body = generic_response
319
- body ? longlong(*body.unpack('NN')) : body
337
+ body ? body.unpack('Q>').first : body
338
+ end
339
+
340
+ def decr(key, count, ttl, default)
341
+ decr_incr :decr, key, count, ttl, default
320
342
  end
321
343
 
322
344
  def incr(key, count, ttl, default)
323
- expiry = default ? ttl : 0xFFFFFFFF
324
- default ||= 0
325
- (h, l) = split(count)
326
- (dh, dl) = split(default)
327
- req = [REQUEST, OPCODES[:incr], key.bytesize, 20, 0, 0, key.bytesize + 20, 0, 0, h, l, dh, dl, expiry, key].pack(FORMAT[:incr])
328
- write(req)
329
- body = generic_response
330
- body ? longlong(*body.unpack('NN')) : body
345
+ decr_incr :incr, key, count, ttl, default
346
+ end
347
+
348
+ def write_append_prepend(opcode, key, value)
349
+ write_generic [REQUEST, OPCODES[opcode], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[opcode])
350
+ end
351
+
352
+ def write_generic(bytes)
353
+ write(bytes)
354
+ generic_response
331
355
  end
332
356
 
333
357
  def write_noop
@@ -343,15 +367,11 @@ module Dalli
343
367
  end
344
368
 
345
369
  def append(key, value)
346
- req = [REQUEST, OPCODES[:append], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[:append])
347
- write(req)
348
- generic_response
370
+ write_append_prepend :append, key, value
349
371
  end
350
372
 
351
373
  def prepend(key, value)
352
- req = [REQUEST, OPCODES[:prepend], key.bytesize, 0, 0, 0, value.bytesize + key.bytesize, 0, 0, key, value].pack(FORMAT[:prepend])
353
- write(req)
354
- generic_response
374
+ write_append_prepend :prepend, key, value
355
375
  end
356
376
 
357
377
  def stats(info='')
@@ -361,9 +381,7 @@ module Dalli
361
381
  end
362
382
 
363
383
  def reset_stats
364
- req = [REQUEST, OPCODES[:stat], 'reset'.bytesize, 0, 0, 0, 'reset'.bytesize, 0, 0, 'reset'].pack(FORMAT[:stat])
365
- write(req)
366
- generic_response
384
+ write_generic [REQUEST, OPCODES[:stat], 'reset'.bytesize, 0, 0, 0, 'reset'.bytesize, 0, 0, 'reset'].pack(FORMAT[:stat])
367
385
  end
368
386
 
369
387
  def cas(key)
@@ -373,15 +391,12 @@ module Dalli
373
391
  end
374
392
 
375
393
  def version
376
- req = [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
377
- write(req)
378
- generic_response
394
+ write_generic [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
379
395
  end
380
396
 
381
397
  def touch(key, ttl)
382
- req = [REQUEST, OPCODES[:touch], key.bytesize, 4, 0, 0, key.bytesize + 4, 0, 0, ttl, key].pack(FORMAT[:touch])
383
- write(req)
384
- generic_response
398
+ ttl = sanitize_ttl(ttl)
399
+ write_generic [REQUEST, OPCODES[:touch], key.bytesize, 4, 0, 0, key.bytesize + 4, 0, 0, ttl, key].pack(FORMAT[:touch])
385
400
  end
386
401
 
387
402
  # http://www.hjp.at/zettel/m/memcached_flags.rxml
@@ -396,6 +411,8 @@ module Dalli
396
411
  marshalled = true
397
412
  begin
398
413
  self.serializer.dump(value)
414
+ rescue Timeout::Error => e
415
+ raise e
399
416
  rescue => ex
400
417
  # Marshalling can throw several different types of generic Ruby exceptions.
401
418
  # Convert to a specific exception so we can special case it higher up the stack.
@@ -407,7 +424,8 @@ module Dalli
407
424
  value.to_s
408
425
  end
409
426
  compressed = false
410
- if @options[:compress] && value.bytesize >= @options[:compression_min_size] &&
427
+ set_compress_option = true if options && options[:compress]
428
+ if (@options[:compress] || set_compress_option) && value.bytesize >= @options[:compression_min_size] &&
411
429
  (!@options[:compression_max_size] || value.bytesize <= @options[:compression_max_size])
412
430
  value = self.compressor.compress(value)
413
431
  compressed = true
@@ -429,14 +447,15 @@ module Dalli
429
447
  rescue ArgumentError
430
448
  raise if $!.message !~ /undefined class|marshal data too short/
431
449
  raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
450
+ rescue NameError
451
+ raise if $!.message !~ /uninitialized constant/
452
+ raise UnmarshalError, "Unable to unmarshal value: #{$!.message}"
432
453
  rescue Zlib::Error
433
454
  raise UnmarshalError, "Unable to uncompress value: #{$!.message}"
434
455
  end
435
456
 
436
457
  def data_cas_response
437
- header = read(24)
438
- raise Dalli::NetworkError, 'No response' if !header
439
- (extras, _, status, count, _, cas) = header.unpack(CAS_HEADER)
458
+ (extras, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
440
459
  data = read(count) if count > 0
441
460
  if status == 1
442
461
  nil
@@ -458,18 +477,35 @@ module Dalli
458
477
  if value.bytesize <= @options[:value_max_bytes]
459
478
  yield
460
479
  else
461
- Dalli.logger.warn "Value for #{key} over max size: #{@options[:value_max_bytes]} <= #{value.bytesize}"
480
+ message = "Value for #{key} over max size: #{@options[:value_max_bytes]} <= #{value.bytesize}"
481
+ raise Dalli::ValueOverMaxSize, message if @options[:error_when_over_max_size]
482
+
483
+ Dalli.logger.error "#{message} - this value may be truncated by memcached"
462
484
  false
463
485
  end
464
486
  end
465
487
 
466
- def generic_response(unpack=false)
467
- header = read(24)
468
- raise Dalli::NetworkError, 'No response' if !header
469
- (extras, _, status, count) = header.unpack(NORMAL_HEADER)
488
+ # https://github.com/memcached/memcached/blob/master/doc/protocol.txt#L79
489
+ # > An expiration time, in seconds. Can be up to 30 days. After 30 days, is treated as a unix timestamp of an exact date.
490
+ MAX_ACCEPTABLE_EXPIRATION_INTERVAL = 30*24*60*60 # 30 days
491
+ def sanitize_ttl(ttl)
492
+ ttl_as_i = ttl.to_i
493
+ return ttl_as_i if ttl_as_i <= MAX_ACCEPTABLE_EXPIRATION_INTERVAL
494
+ now = Time.now.to_i
495
+ return ttl_as_i if ttl_as_i > now # already a timestamp
496
+ Dalli.logger.debug "Expiration interval (#{ttl_as_i}) too long for Memcached, converting to an expiration timestamp"
497
+ now + ttl_as_i
498
+ end
499
+
500
+ # Implements the NullObject pattern to store an application-defined value for 'Key not found' responses.
501
+ class NilObject; end
502
+ NOT_FOUND = NilObject.new
503
+
504
+ def generic_response(unpack=false, cache_nils=false)
505
+ (extras, _, status, count) = read_header.unpack(NORMAL_HEADER)
470
506
  data = read(count) if count > 0
471
507
  if status == 1
472
- nil
508
+ cache_nils ? NOT_FOUND : nil
473
509
  elsif status == 2 || status == 5
474
510
  false # Not stored, normal status for add operation
475
511
  elsif status != 0
@@ -484,9 +520,7 @@ module Dalli
484
520
  end
485
521
 
486
522
  def cas_response
487
- header = read(24)
488
- raise Dalli::NetworkError, 'No response' if !header
489
- (_, _, status, count, _, cas) = header.unpack(CAS_HEADER)
523
+ (_, _, status, count, _, cas) = read_header.unpack(CAS_HEADER)
490
524
  read(count) if count > 0 # this is potential data that we don't care about
491
525
  if status == 1
492
526
  nil
@@ -501,10 +535,8 @@ module Dalli
501
535
 
502
536
  def keyvalue_response
503
537
  hash = {}
504
- loop do
505
- header = read(24)
506
- raise Dalli::NetworkError, 'No response' if !header
507
- (key_length, _, body_length, _) = header.unpack(KV_HEADER)
538
+ while true
539
+ (key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
508
540
  return hash if key_length == 0
509
541
  key = read(key_length)
510
542
  value = read(body_length - key_length) if body_length - key_length > 0
@@ -514,10 +546,8 @@ module Dalli
514
546
 
515
547
  def multi_response
516
548
  hash = {}
517
- loop do
518
- header = read(24)
519
- raise Dalli::NetworkError, 'No response' if !header
520
- (key_length, _, body_length, _) = header.unpack(KV_HEADER)
549
+ while true
550
+ (key_length, _, body_length, _) = read_header.unpack(KV_HEADER)
521
551
  return hash if key_length == 0
522
552
  flags = read(4).unpack('N')[0]
523
553
  key = read(key_length)
@@ -548,14 +578,22 @@ module Dalli
548
578
  end
549
579
  end
550
580
 
581
+ def read_header
582
+ read(24) || raise(Dalli::NetworkError, 'No response')
583
+ end
584
+
551
585
  def connect
552
- Dalli.logger.debug { "Dalli::Server#connect #{hostname}:#{port}" }
586
+ Dalli.logger.debug { "Dalli::Server#connect #{name}" }
553
587
 
554
588
  begin
555
589
  @pid = Process.pid
556
- @sock = KSocket.open(hostname, port, self, options)
557
- @version = version # trigger actual connect
590
+ if socket_type == :unix
591
+ @sock = KSocket::UNIX.open(hostname, self, options)
592
+ else
593
+ @sock = KSocket::TCP.open(hostname, port, self, options)
594
+ end
558
595
  sasl_authentication if need_auth?
596
+ @version = version # trigger actual connect
559
597
  up!
560
598
  rescue Dalli::DalliError # SASL auth failure
561
599
  raise
@@ -569,13 +607,11 @@ module Dalli
569
607
  [n >> 32, 0xFFFFFFFF & n]
570
608
  end
571
609
 
572
- def longlong(a, b)
573
- (a << 32) | b
574
- end
575
-
576
610
  REQUEST = 0x80
577
611
  RESPONSE = 0x81
578
612
 
613
+ # Response codes taken from:
614
+ # https://github.com/memcached/memcached/wiki/BinaryProtocolRevamped#response-status
579
615
  RESPONSE_CODES = {
580
616
  0 => 'No error',
581
617
  1 => 'Key not found',
@@ -584,9 +620,16 @@ module Dalli
584
620
  4 => 'Invalid arguments',
585
621
  5 => 'Item not stored',
586
622
  6 => 'Incr/decr on a non-numeric value',
623
+ 7 => 'The vbucket belongs to another server',
624
+ 8 => 'Authentication error',
625
+ 9 => 'Authentication continue',
587
626
  0x20 => 'Authentication required',
588
627
  0x81 => 'Unknown command',
589
628
  0x82 => 'Out of memory',
629
+ 0x83 => 'Not supported',
630
+ 0x84 => 'Internal error',
631
+ 0x85 => 'Busy',
632
+ 0x86 => 'Temporary failure'
590
633
  }
591
634
 
592
635
  OPCODES = {
@@ -661,11 +704,10 @@ module Dalli
661
704
  # negotiate
662
705
  req = [REQUEST, OPCODES[:auth_negotiation], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
663
706
  write(req)
664
- header = read(24)
665
- raise Dalli::NetworkError, 'No response' if !header
666
- (extras, type, status, count) = header.unpack(NORMAL_HEADER)
707
+
708
+ (extras, _type, status, count) = read_header.unpack(NORMAL_HEADER)
667
709
  raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
668
- content = read(count)
710
+ content = read(count).gsub(/\u0000/, ' ')
669
711
  return (Dalli.logger.debug("Authentication not required/supported by server")) if status == 0x81
670
712
  mechanisms = content.split(' ')
671
713
  raise NotImplementedError, "Dalli only supports the PLAIN authentication mechanism" if !mechanisms.include?('PLAIN')
@@ -676,9 +718,7 @@ module Dalli
676
718
  req = [REQUEST, OPCODES[:auth_request], mechanism.bytesize, 0, 0, 0, mechanism.bytesize + msg.bytesize, 0, 0, mechanism, msg].pack(FORMAT[:auth_request])
677
719
  write(req)
678
720
 
679
- header = read(24)
680
- raise Dalli::NetworkError, 'No response' if !header
681
- (extras, type, status, count) = header.unpack(NORMAL_HEADER)
721
+ (extras, _type, status, count) = read_header.unpack(NORMAL_HEADER)
682
722
  raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
683
723
  content = read(count)
684
724
  return Dalli.logger.info("Dalli/SASL: #{content}") if status == 0
@@ -688,5 +728,25 @@ module Dalli
688
728
  # (step, msg) = sasl.receive('challenge', content)
689
729
  # raise Dalli::NetworkError, "Authentication failed" if sasl.failed? || step != 'response'
690
730
  end
731
+
732
+ def parse_hostname(str)
733
+ res = str.match(/\A(\[([\h:]+)\]|[^:]+)(?::(\d+))?(?::(\d+))?\z/)
734
+ raise Dalli::DalliError, "Could not parse hostname #{str}" if res.nil? || res[1] == '[]'
735
+ hostnam = res[2] || res[1]
736
+ if hostnam =~ /\A\//
737
+ socket_type = :unix
738
+ # in case of unix socket, allow only setting of weight, not port
739
+ raise Dalli::DalliError, "Could not parse hostname #{str}" if res[4]
740
+ weigh = res[3]
741
+ else
742
+ socket_type = :tcp
743
+ por = res[3] || DEFAULT_PORT
744
+ por = Integer(por)
745
+ weigh = res[4]
746
+ end
747
+ weigh ||= DEFAULT_WEIGHT
748
+ weigh = Integer(weigh)
749
+ return hostnam, por, weigh, socket_type
750
+ end
691
751
  end
692
752
  end