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 +5 -0
- data/README.md +16 -2
- data/Rakefile +8 -0
- data/lib/dalli.rb +3 -1
- data/lib/dalli/client.rb +20 -6
- data/lib/dalli/options.rb +2 -1
- data/lib/dalli/ring.rb +14 -12
- data/lib/dalli/server.rb +86 -78
- data/lib/dalli/socket.rb +73 -40
- data/lib/dalli/version.rb +1 -1
- data/test/helper.rb +5 -1
- data/test/memcached_mock.rb +14 -2
- data/test/test_dalli.rb +1 -59
- data/test/test_failover.rb +73 -0
- data/test/test_network.rb +6 -10
- data/test/test_ring.rb +89 -0
- data/test/test_sasl.rb +55 -0
- metadata +16 -3
data/History.md
CHANGED
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 [
|
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
|
-
[
|
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
|
-
|
62
|
-
values
|
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
|
-
|
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
|
-
|
213
|
-
|
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
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
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
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::
|
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
|
-
|
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
|
-
@
|
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!
|
48
|
+
down!
|
36
49
|
end
|
37
50
|
end
|
38
51
|
|
39
52
|
def alive?
|
40
|
-
return true if @sock
|
41
|
-
return false if @down_at && @down_at == Time.now.to_i
|
53
|
+
return true if @sock
|
42
54
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
-
|
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
|
68
|
-
|
83
|
+
def failure!
|
84
|
+
Dalli.logger.info { "#{hostname}:#{port} failed (count: #{@fail_count})" }
|
69
85
|
|
70
|
-
|
71
|
-
|
72
|
-
|
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
|
-
|
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!
|
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
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
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
|
-
|
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
|
-
@
|
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
|
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
|
343
|
+
def write(bytes)
|
337
344
|
begin
|
338
|
-
|
345
|
+
@sock.write(bytes)
|
339
346
|
rescue SystemCallError
|
340
|
-
|
347
|
+
failure!
|
348
|
+
retry
|
341
349
|
end
|
342
350
|
end
|
343
351
|
|
344
|
-
def read(count
|
352
|
+
def read(count)
|
345
353
|
begin
|
346
|
-
|
354
|
+
@sock.readfull(count)
|
347
355
|
rescue SystemCallError, Timeout::Error, EOFError
|
348
|
-
|
356
|
+
failure!
|
357
|
+
retry
|
349
358
|
end
|
350
359
|
end
|
351
360
|
|
352
|
-
def
|
353
|
-
|
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
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
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
|
-
|
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
|
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
|
470
|
-
header = read(24
|
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
|
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
|
-
|
492
|
+
write(req)
|
485
493
|
|
486
|
-
header = read(24
|
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
|
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
|
-
|
7
|
-
|
8
|
-
def
|
9
|
-
IO.select([self], nil, nil,
|
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
|
13
|
-
IO.select(nil, [self], nil,
|
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.
|
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
|
37
|
-
|
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
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
55
|
-
|
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
|
-
|
68
|
+
|
59
69
|
end
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
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
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
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
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
|
data/test/memcached_mock.rb
CHANGED
@@ -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
|
8
|
-
assert_raise Dalli::
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
-
|
9
|
-
version: 0.11.
|
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-
|
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
|