dalli 0.11.1 → 0.11.2

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/History.md CHANGED
@@ -1,6 +1,11 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
+ 0.11.2
5
+ =======
6
+
7
+ - Major reworking of socket error and failover handling (gucki)
8
+
4
9
  0.11.1
5
10
  ======
6
11
 
data/README.md CHANGED
@@ -7,7 +7,7 @@ The name is a variant of Salvador Dali for his famous painting [The Persistence
7
7
 
8
8
  ![Persistence of Memory](http://www.virtualdali.com/assets/paintings/31PersistenceOfMemory.jpg)
9
9
 
10
- Dalli's development is sponsored by [NorthScale](http://www.northscale.com/). Many thanks to them!
10
+ Dalli's development is sponsored by [Membase](http://www.membase.com/). Many thanks to them!
11
11
 
12
12
 
13
13
  Design
@@ -28,6 +28,7 @@ So a few notes. Dalli:
28
28
  3. comes with hooks to replace memcache-client in Rails.
29
29
  4. is approx 700 lines of Ruby. memcache-client is approx 1250 lines.
30
30
  5. supports SASL for use in managed environments, e.g. Heroku.
31
+ 6. provides proper failover with recovery and adjustable timeouts
31
32
 
32
33
 
33
34
  Installation and Usage
@@ -111,6 +112,19 @@ Put this at the bottom of `config/environment.rb`:
111
112
  end
112
113
 
113
114
 
115
+ Configuration
116
+ ------------------------
117
+ Dalli accepts the following options. All times are in seconds and maybe fractional.
118
+
119
+ **socket_timeout**: Timeout for all socket operations (connect, read, write). Default is 0.5.
120
+
121
+ **socket_max_failures**: When a socket operation fails after socket_timeout, the same operation is retried. This is to not immediately mark a server down when there's a very slight network problem. Default is 2.
122
+
123
+ **socket_failure_delay**: Before retrying a socket operation, the process sleeps for this amount of time. Default is 0.01.
124
+
125
+ **down_retry_delay**: When a server has been marked down due to many failures, the server will be checked again for being alive only after this amount of time. Don't set this value to low, otherwise each request which tries the failed server might hang for the maximum timeout (see below). Default is 30 seconds.
126
+
127
+
114
128
  Features and Changes
115
129
  ------------------------
116
130
 
@@ -134,7 +148,7 @@ Eric Wong - for help using his [kgio](http://unicorn.bogomips.org/kgio/index.htm
134
148
 
135
149
  Brian Mitchell - for his remix-stash project which was helpful when implementing and testing the binary protocol support.
136
150
 
137
- [NorthScale](http://northscale.com) - for their project sponsorship
151
+ [Membase](http://membase.com) - for their project sponsorship
138
152
 
139
153
  [Bootspring](http://bootspring.com) is my Ruby and Rails consulting company. We specialize in Ruby infrastructure, performance and scalability tuning for Rails applications. If you need help, please [contact us](mailto:info@bootspring.com) today.
140
154
 
data/Rakefile CHANGED
@@ -9,6 +9,14 @@ Rake::TestTask.new(:bench) do |test|
9
9
  test.pattern = 'test/benchmark_test.rb'
10
10
  end
11
11
 
12
+ begin
13
+ require 'metric_fu'
14
+ MetricFu::Configuration.run do |config|
15
+ config.rcov[:rcov_opts] << "-Itest:lib"
16
+ end
17
+ rescue LoadError
18
+ end
19
+
12
20
  task :default => :test
13
21
 
14
22
  task :test_all do
data/lib/dalli.rb CHANGED
@@ -16,6 +16,8 @@ module Dalli
16
16
  class DalliError < RuntimeError; end
17
17
  # socket/server communication error
18
18
  class NetworkError < DalliError; end
19
+ # no server available/alive error
20
+ class RingError < DalliError; end
19
21
 
20
22
  def self.logger
21
23
  @logger ||= (rails_logger || default_logger)
@@ -36,4 +38,4 @@ module Dalli
36
38
  def self.logger=(logger)
37
39
  @logger = logger
38
40
  end
39
- end
41
+ end
data/lib/dalli/client.rb CHANGED
@@ -58,8 +58,12 @@ module Dalli
58
58
  keys.flatten.each do |key|
59
59
  perform(:getkq, key)
60
60
  end
61
- values = ring.servers.inject({}) { |hash, s| hash.merge!(s.request(:noop)); hash }
62
- values.inject(values) { |memo, (k,v)| memo[k] = v; memo }
61
+
62
+ values = {}
63
+ ring.servers.each do |server|
64
+ values.merge!(server.request(:noop)) if server.alive?
65
+ end
66
+ values
63
67
  end
64
68
  end
65
69
 
@@ -173,7 +177,11 @@ module Dalli
173
177
  # Collect the stats for each server.
174
178
  # Returns a hash like { 'hostname:port' => { 'stat1' => 'value1', ... }, 'hostname2:port' => { ... } }
175
179
  def stats
176
- ring.servers.inject({}) { |memo, s| memo["#{s.hostname}:#{s.port}"] = s.request(:stats); memo }
180
+ values = {}
181
+ ring.servers.each do |server|
182
+ values["#{server.hostname}:#{server.port}"] = server.alive? ? server.request(:stats) : nil
183
+ end
184
+ values
177
185
  end
178
186
 
179
187
  ##
@@ -209,8 +217,14 @@ module Dalli
209
217
  args[0] = key
210
218
  end
211
219
  args[0] = key = validate_key(key)
212
- server = ring.server_for_key(key)
213
- server.request(op, *args)
220
+ begin
221
+ server = ring.server_for_key(key)
222
+ server.request(op, *args)
223
+ rescue NetworkError => e
224
+ Dalli.logger.debug { e.message }
225
+ Dalli.logger.debug { "retrying request with new server" }
226
+ retry
227
+ end
214
228
  end
215
229
 
216
230
  def validate_key(key)
@@ -222,4 +236,4 @@ module Dalli
222
236
  @options[:namespace] ? "#{@options[:namespace]}:#{key}" : key
223
237
  end
224
238
  end
225
- end
239
+ end
data/lib/dalli/options.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'thread'
2
+ require 'monitor'
2
3
 
3
4
  module Dalli
4
5
 
@@ -40,4 +41,4 @@ module Dalli
40
41
  end
41
42
 
42
43
  end
43
- end
44
+ end
data/lib/dalli/ring.rb CHANGED
@@ -28,19 +28,21 @@ module Dalli
28
28
  end
29
29
 
30
30
  def server_for_key(key)
31
- return @servers.first unless @continuum
32
-
33
- hkey = hash_for(key)
34
-
35
- 20.times do |try|
36
- entryidx = self.class.binary_search(@continuum, hkey)
37
- server = @continuum[entryidx].server
38
- return server if server.alive?
39
- break unless @failover
40
- hkey = hash_for("#{try}#{key}")
31
+ if @continuum
32
+ hkey = hash_for(key)
33
+ 20.times do |try|
34
+ entryidx = self.class.binary_search(@continuum, hkey)
35
+ server = @continuum[entryidx].server
36
+ return server if server.alive?
37
+ break unless @failover
38
+ hkey = hash_for("#{try}#{key}")
39
+ end
40
+ else
41
+ server = @servers.first
42
+ return server if server && server.alive?
41
43
  end
42
44
 
43
- raise Dalli::NetworkError, "No servers available"
45
+ raise Dalli::RingError, "No server available"
44
46
  end
45
47
 
46
48
  def lock
@@ -100,4 +102,4 @@ module Dalli
100
102
  end
101
103
 
102
104
  end
103
- end
105
+ end
data/lib/dalli/server.rb CHANGED
@@ -7,21 +7,34 @@ module Dalli
7
7
  attr_accessor :hostname
8
8
  attr_accessor :port
9
9
  attr_accessor :weight
10
+ attr_accessor :options
10
11
 
11
- def initialize(attribs, options=nil)
12
+ DEFAULTS = {
13
+ # seconds between trying to contact a remote server
14
+ :down_retry_delay => 1,
15
+ # connect/read/write timeout for socket operations
16
+ :socket_timeout => 0.5,
17
+ # times a socket operation may fail before considering the server dead
18
+ :socket_max_failures => 2,
19
+ # amount of time to sleep between retries when a failure occurs
20
+ :socket_failure_delay => 0.01
21
+ }
22
+
23
+ def initialize(attribs, options = {})
12
24
  (@hostname, @port, @weight) = attribs.split(':')
13
25
  @port ||= 11211
14
26
  @port = Integer(@port)
15
27
  @weight ||= 1
16
28
  @weight = Integer(@weight)
29
+ @fail_count = 0
17
30
  @down_at = nil
18
- @version = detect_memcached_version
19
- @options = options
20
- Dalli.logger.debug { "#{@hostname}:#{@port} running memcached v#{@version}" }
31
+ @last_down_at = nil
32
+ @options = DEFAULTS.merge(options)
21
33
  end
22
34
 
23
35
  # Chokepoint method for instrumentation
24
36
  def request(op, *args)
37
+ raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}" unless alive?
25
38
  begin
26
39
  send(op, *args)
27
40
  rescue Dalli::NetworkError
@@ -32,26 +45,29 @@ module Dalli
32
45
  Dalli.logger.error "Unexpected exception in Dalli: #{ex.class.name}: #{ex.message}"
33
46
  Dalli.logger.error "This is a bug in Dalli, please enter an issue in Github if it does not already exist."
34
47
  Dalli.logger.error ex.backtrace.join("\n\t")
35
- down!(true)
48
+ down!
36
49
  end
37
50
  end
38
51
 
39
52
  def alive?
40
- return true if @sock && !@sock.closed?
41
- return false if @down_at && @down_at == Time.now.to_i
53
+ return true if @sock
42
54
 
43
- begin
44
- # try to reconnect at most once per second
45
- connection
46
- true
47
- rescue Dalli::NetworkError => dne
48
- Dalli.logger.info(dne.message)
49
- false
55
+ if @last_down_at && @last_down_at + options[:down_retry_delay] >= Time.now
56
+ time = @last_down_at + options[:down_retry_delay] - Time.now
57
+ Dalli.logger.debug { "down_retry_delay not reached for #{hostname}:#{port} (%.3f seconds left)" % time }
58
+ return false
50
59
  end
60
+
61
+ connect
62
+ @sock
63
+ rescue Dalli::NetworkError
64
+ false
51
65
  end
52
66
 
53
67
  def close
54
- (@sock.close rescue nil; @sock = nil) if @sock
68
+ return unless @sock
69
+ @sock.close rescue nil
70
+ @sock = nil
55
71
  end
56
72
 
57
73
  def lock!
@@ -64,49 +80,46 @@ module Dalli
64
80
 
65
81
  private
66
82
 
67
- def detect_memcached_version
68
- return "(unknown)" if ENV['SKIP_MEMCACHE_VERSION_CHECK']
83
+ def failure!
84
+ Dalli.logger.info { "#{hostname}:#{port} failed (count: #{@fail_count})" }
69
85
 
70
- # HACK, the server does not appear to have a way to negotiate the protocol.
71
- # If you ask for the version in text, the socket is immediately locked to the text
72
- # protocol. But if we use binary, an old remote server will not respond. If
73
- # the server is using SASL, it will not respond to the text protocol. Sigh.
74
- if username
75
- # using SASL, assume the binary protocol will work.
76
- binary_version
86
+ @fail_count += 1
87
+ if @fail_count >= options[:socket_max_failures]
88
+ down!
77
89
  else
78
- # Use text to determine the remote version, close the socket and open it back up.
79
- # Alternative suggestions welcome.
80
- version = text_version
81
- if version < '1.4.0'
82
- Dalli.logger.error "Dalli does not support memcached versions < 1.4.0, found #{version} at #{@hostname}:#{@port}"
83
- raise NotImplementedError, "Dalli does not support memcached versions < 1.4.0, found #{version} at #{@hostname}:#{@port}"
84
- end
85
- close
86
- connection
87
- version
90
+ sleep(options[:socket_failure_delay]) if options[:socket_failure_delay]
88
91
  end
89
92
  end
90
-
91
- def down!(toss_exception=false)
93
+
94
+ def down!
92
95
  close
93
- @down_at = Time.now.to_i
94
- @error = $! && $!.class.name
95
- @msg = @msg || ($! && $!.message && !$!.message.empty? && $!.message)
96
- @trace = @trace || $!.backtrace
97
96
 
98
- if toss_exception
99
- x = Dalli::NetworkError.new("#{self.hostname}:#{self.port} is currently down: #{@error} #{@msg}")
100
- x.set_backtrace @trace
101
- raise x
97
+ @last_down_at = Time.now
98
+
99
+ if @down_at
100
+ time = Time.now - @down_at
101
+ Dalli.logger.debug { "#{hostname}:#{port} is still down (for %.3f seconds now)" % time }
102
+ else
103
+ @down_at = @last_down_at
104
+ Dalli.logger.warn { "#{hostname}:#{port} is down" }
102
105
  end
103
- nil
106
+
107
+ @error = $! && $!.class.name
108
+ @msg = @msg || ($! && $!.message && !$!.message.empty? && $!.message)
109
+ raise Dalli::NetworkError, "#{hostname}:#{port} is down: #{@error} #{@msg}"
104
110
  end
105
111
 
106
112
  def up!
113
+ if @down_at
114
+ time = Time.now - @down_at
115
+ Dalli.logger.warn { "#{hostname}:#{port} is back (downtime was %.3f seconds)" % time }
116
+ end
117
+
118
+ @fail_count = 0
107
119
  @down_at = nil
120
+ @last_down_at = nil
108
121
  @msg = nil
109
- @trace = nil
122
+ @error = nil
110
123
  end
111
124
 
112
125
  def multi?
@@ -215,13 +228,7 @@ module Dalli
215
228
  cas_response
216
229
  end
217
230
 
218
- def text_version
219
- write("version\r\n")
220
- connection.gets =~ /VERSION (.*)\r\n/
221
- $1
222
- end
223
-
224
- def binary_version
231
+ def version
225
232
  req = [REQUEST, OPCODES[:version], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
226
233
  write(req)
227
234
  generic_response
@@ -333,36 +340,37 @@ module Dalli
333
340
  end
334
341
  end
335
342
 
336
- def write(bytes, socket=connection)
343
+ def write(bytes)
337
344
  begin
338
- socket.write(bytes)
345
+ @sock.write(bytes)
339
346
  rescue SystemCallError
340
- down!(true)
347
+ failure!
348
+ retry
341
349
  end
342
350
  end
343
351
 
344
- def read(count, socket=connection)
352
+ def read(count)
345
353
  begin
346
- socket.readfull(count)
354
+ @sock.readfull(count)
347
355
  rescue SystemCallError, Timeout::Error, EOFError
348
- down!(true)
356
+ failure!
357
+ retry
349
358
  end
350
359
  end
351
360
 
352
- def connection
353
- @sock ||= begin
354
- if @down_at && @down_at == Time.now.to_i
355
- raise Dalli::NetworkError, "#{self.hostname}:#{self.port} is currently down: #{@msg}"
356
- end
361
+ def connect
362
+ Dalli.logger.debug { "Dalli::Server#connect #{hostname}:#{port}" }
357
363
 
358
- begin
359
- sock = KSocket.open(hostname, port)
360
- rescue
361
- down!(true)
362
- end
363
- sasl_authentication(sock) if Dalli::Server.need_auth?
364
+ begin
365
+ @sock = KSocket.open(hostname, port, :timeout => options[:socket_timeout])
366
+ @version = version # trigger actual connect
367
+ sasl_authentication if Dalli::Server.need_auth?
364
368
  up!
365
- sock
369
+ rescue Dalli::DalliError # SASL auth failure
370
+ raise
371
+ rescue SystemCallError, Timeout::Error, EOFError
372
+ failure!
373
+ retry
366
374
  end
367
375
  end
368
376
 
@@ -459,19 +467,19 @@ module Dalli
459
467
  ENV['MEMCACHE_PASSWORD']
460
468
  end
461
469
 
462
- def sasl_authentication(socket)
470
+ def sasl_authentication
463
471
  init_sasl if !defined?(::SASL)
464
472
 
465
473
  Dalli.logger.info { "Dalli/SASL authenticating as #{username}" }
466
474
 
467
475
  # negotiate
468
476
  req = [REQUEST, OPCODES[:auth_negotiation], 0, 0, 0, 0, 0, 0, 0].pack(FORMAT[:noop])
469
- write(req, socket)
470
- header = read(24, socket)
477
+ write(req)
478
+ header = read(24)
471
479
  raise Dalli::NetworkError, 'No response' if !header
472
480
  (extras, type, status, count) = header.unpack(NORMAL_HEADER)
473
481
  raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
474
- content = read(count, socket)
482
+ content = read(count)
475
483
  return (Dalli.logger.debug("Authentication not required/supported by server")) if status == 0x81
476
484
  mechanisms = content.split(' ')
477
485
 
@@ -481,13 +489,13 @@ module Dalli
481
489
  mechanism = sasl.name
482
490
  #p [mechanism, msg]
483
491
  req = [REQUEST, OPCODES[:auth_request], mechanism.bytesize, 0, 0, 0, mechanism.bytesize + msg.bytesize, 0, 0, mechanism, msg].pack(FORMAT[:auth_request])
484
- socket.write(req)
492
+ write(req)
485
493
 
486
- header = read(24, socket)
494
+ header = read(24)
487
495
  raise Dalli::NetworkError, 'No response' if !header
488
496
  (extras, type, status, count) = header.unpack(NORMAL_HEADER)
489
497
  raise Dalli::NetworkError, "Unexpected message format: #{extras} #{count}" unless extras == 0 && count > 0
490
- content = read(count, socket)
498
+ content = read(count)
491
499
  return Dalli.logger.info("Dalli/SASL: #{content}") if status == 0
492
500
 
493
501
  raise Dalli::DalliError, "Error authenticating: #{status}" unless status == 0x21
data/lib/dalli/socket.rb CHANGED
@@ -3,20 +3,21 @@ begin
3
3
  puts "Using kgio socket IO" if $TESTING
4
4
 
5
5
  class Dalli::Server::KSocket < Kgio::Socket
6
- TIMEOUT = 0.5
7
-
8
- def wait_readable
9
- IO.select([self], nil, nil, TIMEOUT) || raise(Timeout::Error, "IO timeout")
6
+ attr_accessor :options
7
+
8
+ def kgio_wait_readable
9
+ IO.select([self], nil, nil, options[:timeout]) || raise(Timeout::Error, "IO timeout")
10
10
  end
11
11
 
12
- def wait_writable
13
- IO.select(nil, [self], nil, TIMEOUT) || raise(Timeout::Error, "IO timeout")
12
+ def kgio_wait_writable
13
+ IO.select(nil, [self], nil, options[:timeout]) || raise(Timeout::Error, "IO timeout")
14
14
  end
15
15
 
16
- def self.open(host, port)
16
+ def self.open(host, port, options = {})
17
17
  addr = Socket.pack_sockaddr_in(port, host)
18
18
  sock = start(addr)
19
- sock.wait_writable
19
+ sock.options = options
20
+ sock.kgio_wait_writable
20
21
  sock
21
22
  end
22
23
 
@@ -33,48 +34,80 @@ begin
33
34
 
34
35
  end
35
36
 
36
- ::Kgio.wait_readable = :wait_readable
37
- ::Kgio.wait_writable = :wait_writable
37
+ if ::Kgio.respond_to?(:wait_readable=)
38
+ ::Kgio.wait_readable = :kgio_wait_readable
39
+ ::Kgio.wait_writable = :kgio_wait_writable
40
+ end
38
41
 
39
42
  rescue LoadError
40
- puts "Using standard socket IO" if $TESTING
41
-
42
- class Dalli::Server::KSocket < Socket
43
- TIMEOUT = 0.5
44
-
45
- def self.open(host, port)
46
- # All this ugly code to ensure proper Socket connect timeout
47
- addr = Socket.getaddrinfo(host, nil)
48
- sock = new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
49
- begin
50
- sock.connect_nonblock(Socket.pack_sockaddr_in(port, addr[0][3]))
51
- rescue Errno::EINPROGRESS
52
- resp = IO.select(nil, [sock], nil, TIMEOUT)
43
+
44
+ puts "Using standard socket IO (#{RUBY_DESCRIPTION})" if $TESTING
45
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
46
+
47
+ class Dalli::Server::KSocket < TCPSocket
48
+ def self.open(host, port, options = {})
49
+ new(host, port)
50
+ end
51
+
52
+ def readfull(count)
53
+ value = ''
53
54
  begin
54
- sock.connect_nonblock(Socket.pack_sockaddr_in(port, addr[0][3]))
55
- rescue Errno::EISCONN
55
+ loop do
56
+ value << sysread(count - value.bytesize)
57
+ break if value.bytesize == count
58
+ end
59
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
60
+ if IO.select([self], nil, nil, options[:timeout])
61
+ retry
62
+ else
63
+ raise Timeout::Error, "IO timeout"
64
+ end
56
65
  end
66
+ value
57
67
  end
58
- sock
68
+
59
69
  end
60
-
61
- def readfull(count)
62
- value = ''
63
- begin
64
- loop do
65
- value << sysread(count - value.bytesize)
66
- break if value.bytesize == count
70
+
71
+ else
72
+
73
+ class Dalli::Server::KSocket < Socket
74
+ attr_accessor :options
75
+
76
+ def self.open(host, port, options = {})
77
+ # All this ugly code to ensure proper Socket connect timeout
78
+ addr = Socket.getaddrinfo(host, nil)
79
+ sock = new(Socket.const_get(addr[0][0]), Socket::SOCK_STREAM, 0)
80
+ sock.options = options
81
+ begin
82
+ sock.connect_nonblock(Socket.pack_sockaddr_in(port, addr[0][3]))
83
+ rescue Errno::EINPROGRESS
84
+ resp = IO.select(nil, [sock], nil, sock.options[:timeout])
85
+ begin
86
+ sock.connect_nonblock(Socket.pack_sockaddr_in(port, addr[0][3]))
87
+ rescue Errno::EISCONN
88
+ end
67
89
  end
68
- rescue Errno::EAGAIN, Errno::EWOULDBLOCK
69
- if IO.select([self], nil, nil, TIMEOUT)
70
- retry
71
- else
72
- raise Timeout::Error, "IO timeout"
90
+ sock
91
+ end
92
+
93
+ def readfull(count)
94
+ value = ''
95
+ begin
96
+ loop do
97
+ value << sysread(count - value.bytesize)
98
+ break if value.bytesize == count
99
+ end
100
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK
101
+ if IO.select([self], nil, nil, options[:timeout])
102
+ retry
103
+ else
104
+ raise Timeout::Error, "IO timeout"
105
+ end
73
106
  end
107
+ value
74
108
  end
75
- value
76
109
  end
77
110
 
78
111
  end
79
112
 
80
- end
113
+ end
data/lib/dalli/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Dalli
2
- VERSION = '0.11.1'
2
+ VERSION = '0.11.2'
3
3
  end
data/test/helper.rb CHANGED
@@ -12,6 +12,10 @@ require 'memcached_mock'
12
12
  require 'mocha'
13
13
 
14
14
  require 'dalli'
15
+ require 'logger'
16
+
17
+ Dalli.logger = Logger.new(STDOUT)
18
+ Dalli.logger.level = Logger::ERROR
15
19
 
16
20
  class Test::Unit::TestCase
17
21
  include MemcachedMock::Helper
@@ -47,4 +51,4 @@ class Test::Unit::TestCase
47
51
  yield
48
52
  end
49
53
 
50
- end
54
+ end
@@ -75,7 +75,7 @@ module MemcachedMock
75
75
  begin
76
76
  Process.kill("TERM", pid)
77
77
  Process.wait(pid)
78
- rescue Errno::ECHILD
78
+ rescue Errno::ECHILD, Errno::ESRCH
79
79
  end
80
80
  end
81
81
  sleep 0.1
@@ -84,6 +84,18 @@ module MemcachedMock
84
84
 
85
85
  yield Dalli::Client.new(["localhost:#{port}", "127.0.0.1:#{port}"])
86
86
  end
87
+
88
+ def memcached_kill(port)
89
+ pid = $started.delete(port)
90
+ if pid
91
+ begin
92
+ Process.kill("TERM", pid)
93
+ Process.wait(pid)
94
+ rescue Errno::ECHILD, Errno::ESRCH
95
+ end
96
+ end
97
+ end
98
+
87
99
  end
88
100
  end
89
101
 
@@ -91,4 +103,4 @@ module Memcached
91
103
  class << self
92
104
  attr_accessor :path
93
105
  end
94
- end
106
+ end
data/test/test_dalli.rb CHANGED
@@ -2,7 +2,7 @@ require 'helper'
2
2
  require 'memcached_mock'
3
3
 
4
4
  class TestDalli < Test::Unit::TestCase
5
-
5
+
6
6
  should "default to localhost:11211" do
7
7
  dc = Dalli::Client.new
8
8
  ring = dc.send(:ring)
@@ -26,17 +26,6 @@ class TestDalli < Test::Unit::TestCase
26
26
  assert_equal s2, s3
27
27
  end
28
28
 
29
- should "have the continuum sorted by value" do
30
- servers = [stub(:hostname => "localhost", :port => "11211", :weight => 1),
31
- stub(:hostname => "localhost", :port => "9500", :weight => 1)]
32
- ring = Dalli::Ring.new(servers, {})
33
- previous_value = 0
34
- ring.continuum.each do |entry|
35
- assert entry.value > previous_value
36
- previous_value = entry.value
37
- end
38
- end
39
-
40
29
  context 'using a live server' do
41
30
 
42
31
  should "support get/set" do
@@ -345,53 +334,6 @@ class TestDalli < Test::Unit::TestCase
345
334
  end
346
335
  end
347
336
 
348
- context 'without authentication credentials' do
349
- setup do
350
- ENV['MEMCACHE_USERNAME'] = 'testuser'
351
- ENV['MEMCACHE_PASSWORD'] = 'wrongpwd'
352
- end
353
-
354
- teardown do
355
- ENV['MEMCACHE_USERNAME'] = nil
356
- ENV['MEMCACHE_PASSWORD'] = nil
357
- end
358
-
359
- should 'gracefully handle authentication failures' do
360
- memcached(19124, '-S') do |dc|
361
- assert_raise Dalli::DalliError, /32/ do
362
- dc.set('abc', 123)
363
- end
364
- end
365
- end
366
- end
367
-
368
- # OSX: Create a SASL user for the memcached application like so:
369
- #
370
- # saslpasswd2 -a memcached -c testuser
371
- #
372
- # with password 'testtest'
373
- context 'in an authenticated environment' do
374
- setup do
375
- ENV['MEMCACHE_USERNAME'] = 'testuser'
376
- ENV['MEMCACHE_PASSWORD'] = 'testtest'
377
- end
378
-
379
- teardown do
380
- ENV['MEMCACHE_USERNAME'] = nil
381
- ENV['MEMCACHE_PASSWORD'] = nil
382
- end
383
-
384
- should 'support SASL authentication' do
385
- memcached(19124, '-S') do |dc|
386
- # I get "Dalli::DalliError: Error authenticating: 32" in OSX
387
- # but SASL works on Heroku servers. YMMV.
388
- assert_equal true, dc.set('abc', 123)
389
- assert_equal 123, dc.get('abc')
390
- assert_equal({"localhost:19121"=>{}}, dc.stats)
391
- end
392
- end
393
- end
394
-
395
337
  context 'in low memory conditions' do
396
338
 
397
339
  should 'handle error response correctly' do
@@ -0,0 +1,73 @@
1
+ require 'helper'
2
+
3
+ class TestFailover < Test::Unit::TestCase
4
+ context 'assuming some bad servers' do
5
+ should 'handle graceful failover' do
6
+ memcached(29125) do
7
+ memcached(29126) do
8
+ dc = Dalli::Client.new ['localhost:29125', 'localhost:29126']
9
+ dc.set 'foo', 'bar'
10
+ foo = dc.get 'foo'
11
+ assert foo, 'bar'
12
+
13
+ memcached_kill(29125)
14
+
15
+ dc.set 'foo', 'bar'
16
+ foo = dc.get 'foo'
17
+ assert foo, 'bar'
18
+
19
+ memcached_kill(29126)
20
+
21
+ assert_raise Dalli::RingError, :message => "No server available" do
22
+ dc.set 'foo', 'bar'
23
+ end
24
+ end
25
+ end
26
+ end
27
+
28
+ should 'handle graceful failover in get_multi' do
29
+ memcached(29125) do
30
+ memcached(29126) do
31
+ dc = Dalli::Client.new ['localhost:29125', 'localhost:29126']
32
+ dc.set 'foo', 'foo1'
33
+ dc.set 'bar', 'bar1'
34
+ result = dc.get_multi ['foo', 'bar']
35
+ assert_equal result, {'foo' => 'foo1', 'bar' => 'bar1'}
36
+
37
+ memcached_kill(29125)
38
+
39
+ dc.set 'foo', 'foo1'
40
+ dc.set 'bar', 'bar1'
41
+ result = dc.get_multi ['foo', 'bar']
42
+ assert_equal result, {'foo' => 'foo1', 'bar' => 'bar1'}
43
+
44
+ memcached_kill(29126)
45
+
46
+ assert_raise Dalli::RingError, :message => "No server available" do
47
+ dc.get_multi ['foo', 'bar']
48
+ end
49
+ end
50
+ end
51
+ end
52
+
53
+ should 'stats should still properly report' do
54
+ memcached(29125) do
55
+ memcached(29126) do
56
+ dc = Dalli::Client.new ['localhost:29125', 'localhost:29126']
57
+ result = dc.stats
58
+ assert_instance_of Hash, result['localhost:29125']
59
+ assert_instance_of Hash, result['localhost:29126']
60
+
61
+ memcached_kill(29125)
62
+
63
+ dc = Dalli::Client.new ['localhost:29125', 'localhost:29126']
64
+ result = dc.stats
65
+ assert_instance_of NilClass, result['localhost:29125']
66
+ assert_instance_of Hash, result['localhost:29126']
67
+
68
+ memcached_kill(29126)
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
data/test/test_network.rb CHANGED
@@ -4,21 +4,17 @@ class TestNetwork < Test::Unit::TestCase
4
4
 
5
5
  context 'assuming a bad network' do
6
6
 
7
- should 'handle connection refused' do
8
- assert_raise Dalli::NetworkError do
7
+ should 'handle no server available' do
8
+ assert_raise Dalli::RingError, :message => "No server available" do
9
9
  dc = Dalli::Client.new 'localhost:19333'
10
10
  dc.get 'foo'
11
11
  end
12
12
  end
13
13
 
14
14
  context 'with a fake server' do
15
- setup do
16
- Dalli::Server.any_instance.expects(:detect_memcached_version).returns('1.4.5')
17
- end
18
-
19
15
  should 'handle connection reset' do
20
16
  memcached_mock(lambda {|sock| sock.close }) do
21
- assert_error Dalli::NetworkError, /Connection reset|EOFError/ do
17
+ assert_raise Dalli::RingError, :message => "No server available" do
22
18
  dc = Dalli::Client.new('localhost:19123')
23
19
  dc.get('abc')
24
20
  end
@@ -27,7 +23,7 @@ class TestNetwork < Test::Unit::TestCase
27
23
 
28
24
  should 'handle malformed response' do
29
25
  memcached_mock(lambda {|sock| sock.write('123') }) do
30
- assert_error Dalli::NetworkError, /EOFError/ do
26
+ assert_raise Dalli::RingError, :message => "No server available" do
31
27
  dc = Dalli::Client.new('localhost:19123')
32
28
  dc.get('abc')
33
29
  end
@@ -36,7 +32,7 @@ class TestNetwork < Test::Unit::TestCase
36
32
 
37
33
  should 'handle connect timeouts' do
38
34
  memcached_mock(lambda {|sock| sleep(0.6); sock.close }, :delayed_start) do
39
- assert_error Dalli::NetworkError, /IO timeout/ do
35
+ assert_raise Dalli::RingError, :message => "No server available" do
40
36
  dc = Dalli::Client.new('localhost:19123')
41
37
  dc.get('abc')
42
38
  end
@@ -45,7 +41,7 @@ class TestNetwork < Test::Unit::TestCase
45
41
 
46
42
  should 'handle read timeouts' do
47
43
  memcached_mock(lambda {|sock| sleep(0.6); sock.write('giraffe') }) do
48
- assert_error Dalli::NetworkError, /IO timeout/ do
44
+ assert_raise Dalli::RingError, :message => "No server available" do
49
45
  dc = Dalli::Client.new('localhost:19123')
50
46
  dc.get('abc')
51
47
  end
data/test/test_ring.rb ADDED
@@ -0,0 +1,89 @@
1
+ require 'helper'
2
+
3
+ class TestRing < Test::Unit::TestCase
4
+
5
+ context 'a ring of servers' do
6
+
7
+ should "have the continuum sorted by value" do
8
+ servers = [stub(:hostname => "localhost", :port => "11211", :weight => 1),
9
+ stub(:hostname => "localhost", :port => "9500", :weight => 1)]
10
+ ring = Dalli::Ring.new(servers, {})
11
+ previous_value = 0
12
+ ring.continuum.each do |entry|
13
+ assert entry.value > previous_value
14
+ previous_value = entry.value
15
+ end
16
+ end
17
+
18
+ should 'raise when no servers are available/defined' do
19
+ ring = Dalli::Ring.new([], {})
20
+ assert_raise Dalli::RingError, :message => "No server available" do
21
+ ring.server_for_key('test')
22
+ end
23
+ end
24
+
25
+ context 'containing only a single server' do
26
+ should "raise correctly when it's not alive" do
27
+ servers = [
28
+ Dalli::Server.new("localhost:12345"),
29
+ ]
30
+ ring = Dalli::Ring.new(servers, {})
31
+ assert_raise Dalli::RingError, :message => "No server available" do
32
+ ring.server_for_key('test')
33
+ end
34
+ end
35
+
36
+ should "return the server when it's alive" do
37
+ servers = [
38
+ Dalli::Server.new("localhost:19191"),
39
+ ]
40
+ ring = Dalli::Ring.new(servers, {})
41
+ memcached(19191) do |mc|
42
+ ring = mc.send(:ring)
43
+ assert_equal ring.servers.first.port, ring.server_for_key('test').port
44
+ end
45
+ end
46
+ end
47
+
48
+ context 'containing multiple servers' do
49
+ should "raise correctly when no server is alive" do
50
+ servers = [
51
+ Dalli::Server.new("localhost:12345"),
52
+ Dalli::Server.new("localhost:12346"),
53
+ ]
54
+ ring = Dalli::Ring.new(servers, {})
55
+ assert_raise Dalli::RingError, :message => "No server available" do
56
+ ring.server_for_key('test')
57
+ end
58
+ end
59
+
60
+ should "return an alive server when at least one is alive" do
61
+ servers = [
62
+ Dalli::Server.new("localhost:12346"),
63
+ Dalli::Server.new("localhost:19191"),
64
+ ]
65
+ ring = Dalli::Ring.new(servers, {})
66
+ memcached(19191) do |mc|
67
+ ring = mc.send(:ring)
68
+ assert_equal ring.servers.first.port, ring.server_for_key('test').port
69
+ end
70
+ end
71
+ end
72
+
73
+ should 'detect when a dead server is up again' do
74
+ memcached(29125) do
75
+ down_retry_delay = 0.5
76
+ dc = Dalli::Client.new(['localhost:29125', 'localhost:29126'], :down_retry_delay => down_retry_delay)
77
+ assert_equal 1, dc.stats.values.compact.count
78
+
79
+ memcached(29126) do
80
+ assert_equal 1, dc.stats.values.compact.count
81
+
82
+ sleep(down_retry_delay+0.1)
83
+
84
+ assert_equal 2, dc.stats.values.compact.count
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
data/test/test_sasl.rb ADDED
@@ -0,0 +1,55 @@
1
+ require 'helper'
2
+
3
+ class TestSasl < Test::Unit::TestCase
4
+
5
+ context 'a server requiring authentication' do
6
+
7
+ context 'without authentication credentials' do
8
+ setup do
9
+ ENV['MEMCACHE_USERNAME'] = 'testuser'
10
+ ENV['MEMCACHE_PASSWORD'] = 'wrongpwd'
11
+ end
12
+
13
+ teardown do
14
+ ENV['MEMCACHE_USERNAME'] = nil
15
+ ENV['MEMCACHE_PASSWORD'] = nil
16
+ end
17
+
18
+ should 'gracefully handle authentication failures' do
19
+ memcached(19124, '-S') do |dc|
20
+ assert_raise Dalli::DalliError, /32/ do
21
+ dc.set('abc', 123)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ # OSX: Create a SASL user for the memcached application like so:
28
+ #
29
+ # saslpasswd2 -a memcached -c testuser
30
+ #
31
+ # with password 'testtest'
32
+ context 'in an authenticated environment' do
33
+ setup do
34
+ ENV['MEMCACHE_USERNAME'] = 'testuser'
35
+ ENV['MEMCACHE_PASSWORD'] = 'testtest'
36
+ end
37
+
38
+ teardown do
39
+ ENV['MEMCACHE_USERNAME'] = nil
40
+ ENV['MEMCACHE_PASSWORD'] = nil
41
+ end
42
+
43
+ should 'support SASL authentication' do
44
+ memcached(19124, '-S') do |dc|
45
+ # I get "Dalli::DalliError: Error authenticating: 32" in OSX
46
+ # but SASL works on Heroku servers. YMMV.
47
+ assert_equal true, dc.set('abc', 123)
48
+ assert_equal 123, dc.get('abc')
49
+ assert_equal({"localhost:19121"=>{}}, dc.stats)
50
+ end
51
+ end
52
+ end
53
+
54
+ end
55
+ end
metadata CHANGED
@@ -1,12 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dalli
3
3
  version: !ruby/object:Gem::Version
4
+ hash: 55
4
5
  prerelease: false
5
6
  segments:
6
7
  - 0
7
8
  - 11
8
- - 1
9
- version: 0.11.1
9
+ - 2
10
+ version: 0.11.2
10
11
  platform: ruby
11
12
  authors:
12
13
  - Mike Perham
@@ -14,7 +15,7 @@ autorequire:
14
15
  bindir: bin
15
16
  cert_chain: []
16
17
 
17
- date: 2010-11-14 00:00:00 -08:00
18
+ date: 2010-11-19 00:00:00 -08:00
18
19
  default_executable:
19
20
  dependencies:
20
21
  - !ruby/object:Gem::Dependency
@@ -25,6 +26,7 @@ dependencies:
25
26
  requirements:
26
27
  - - ">="
27
28
  - !ruby/object:Gem::Version
29
+ hash: 3
28
30
  segments:
29
31
  - 0
30
32
  version: "0"
@@ -38,6 +40,7 @@ dependencies:
38
40
  requirements:
39
41
  - - ">="
40
42
  - !ruby/object:Gem::Version
43
+ hash: 3
41
44
  segments:
42
45
  - 0
43
46
  version: "0"
@@ -51,6 +54,7 @@ dependencies:
51
54
  requirements:
52
55
  - - ">="
53
56
  - !ruby/object:Gem::Version
57
+ hash: 5
54
58
  segments:
55
59
  - 3
56
60
  - 0
@@ -66,6 +70,7 @@ dependencies:
66
70
  requirements:
67
71
  - - ">="
68
72
  - !ruby/object:Gem::Version
73
+ hash: 61
69
74
  segments:
70
75
  - 1
71
76
  - 8
@@ -111,7 +116,10 @@ files:
111
116
  - test/test_active_support.rb
112
117
  - test/test_dalli.rb
113
118
  - test/test_encoding.rb
119
+ - test/test_failover.rb
114
120
  - test/test_network.rb
121
+ - test/test_ring.rb
122
+ - test/test_sasl.rb
115
123
  - test/test_session_store.rb
116
124
  has_rdoc: true
117
125
  homepage: http://github.com/mperham/dalli
@@ -127,6 +135,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
127
135
  requirements:
128
136
  - - ">="
129
137
  - !ruby/object:Gem::Version
138
+ hash: 3
130
139
  segments:
131
140
  - 0
132
141
  version: "0"
@@ -135,6 +144,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
135
144
  requirements:
136
145
  - - ">="
137
146
  - !ruby/object:Gem::Version
147
+ hash: 3
138
148
  segments:
139
149
  - 0
140
150
  version: "0"
@@ -153,5 +163,8 @@ test_files:
153
163
  - test/test_active_support.rb
154
164
  - test/test_dalli.rb
155
165
  - test/test_encoding.rb
166
+ - test/test_failover.rb
156
167
  - test/test_network.rb
168
+ - test/test_ring.rb
169
+ - test/test_sasl.rb
157
170
  - test/test_session_store.rb