redis 3.2.2 → 3.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +41 -11
- data/CHANGELOG.md +19 -0
- data/README.md +85 -3
- data/lib/redis.rb +117 -74
- data/lib/redis/client.rb +32 -14
- data/lib/redis/connection/hiredis.rb +3 -1
- data/lib/redis/connection/ruby.rb +91 -2
- data/lib/redis/connection/synchrony.rb +11 -2
- data/lib/redis/subscribe.rb +10 -2
- data/lib/redis/version.rb +1 -1
- data/redis.gemspec +2 -2
- data/test/client_test.rb +59 -0
- data/test/connection_handling_test.rb +27 -0
- data/test/distributed_internals_test.rb +1 -1
- data/test/internals_test.rb +20 -0
- data/test/lint/blocking_commands.rb +1 -1
- data/test/lint/sorted_sets.rb +4 -4
- data/test/publish_subscribe_test.rb +28 -0
- data/test/sentinel_command_test.rb +1 -1
- data/test/sentinel_test.rb +1 -1
- data/test/ssl_test.rb +66 -0
- data/test/support/redis_mock.rb +13 -2
- data/test/support/ssl/gen_certs.sh +31 -0
- data/test/support/ssl/trusted-ca.crt +25 -0
- data/test/support/ssl/trusted-ca.key +27 -0
- data/test/support/ssl/trusted-cert.crt +81 -0
- data/test/support/ssl/trusted-cert.key +28 -0
- data/test/support/ssl/untrusted-ca.crt +26 -0
- data/test/support/ssl/untrusted-ca.key +27 -0
- data/test/support/ssl/untrusted-cert.crt +82 -0
- data/test/support/ssl/untrusted-cert.key +28 -0
- data/test/thread_safety_test.rb +30 -0
- metadata +33 -11
data/lib/redis/client.rb
CHANGED
@@ -12,7 +12,6 @@ class Redis
|
|
12
12
|
:port => 6379,
|
13
13
|
:path => nil,
|
14
14
|
:timeout => 5.0,
|
15
|
-
:connect_timeout => 5.0,
|
16
15
|
:password => nil,
|
17
16
|
:db => 0,
|
18
17
|
:driver => nil,
|
@@ -42,8 +41,16 @@ class Redis
|
|
42
41
|
@options[:path]
|
43
42
|
end
|
44
43
|
|
44
|
+
def read_timeout
|
45
|
+
@options[:read_timeout]
|
46
|
+
end
|
47
|
+
|
48
|
+
def connect_timeout
|
49
|
+
@options[:connect_timeout]
|
50
|
+
end
|
51
|
+
|
45
52
|
def timeout
|
46
|
-
@options[:
|
53
|
+
@options[:read_timeout]
|
47
54
|
end
|
48
55
|
|
49
56
|
def password
|
@@ -109,21 +116,21 @@ class Redis
|
|
109
116
|
path || "#{host}:#{port}"
|
110
117
|
end
|
111
118
|
|
112
|
-
def call(command
|
119
|
+
def call(command)
|
113
120
|
reply = process([command]) { read }
|
114
121
|
raise reply if reply.is_a?(CommandError)
|
115
122
|
|
116
|
-
if
|
117
|
-
|
123
|
+
if block_given?
|
124
|
+
yield reply
|
118
125
|
else
|
119
126
|
reply
|
120
127
|
end
|
121
128
|
end
|
122
129
|
|
123
|
-
def call_loop(command)
|
130
|
+
def call_loop(command, timeout = 0)
|
124
131
|
error = nil
|
125
132
|
|
126
|
-
result =
|
133
|
+
result = with_socket_timeout(timeout) do
|
127
134
|
process([command]) do
|
128
135
|
loop do
|
129
136
|
reply = read
|
@@ -175,15 +182,21 @@ class Redis
|
|
175
182
|
reconnect = @reconnect
|
176
183
|
|
177
184
|
begin
|
185
|
+
exception = nil
|
186
|
+
|
178
187
|
process(commands) do
|
179
188
|
result[0] = read
|
180
189
|
|
181
190
|
@reconnect = false
|
182
191
|
|
183
192
|
(commands.size - 1).times do |i|
|
184
|
-
|
193
|
+
reply = read
|
194
|
+
result[i + 1] = reply
|
195
|
+
exception = reply if exception.nil? && reply.is_a?(CommandError)
|
185
196
|
end
|
186
197
|
end
|
198
|
+
|
199
|
+
raise exception if exception
|
187
200
|
ensure
|
188
201
|
@reconnect = reconnect
|
189
202
|
end
|
@@ -392,7 +405,7 @@ class Redis
|
|
392
405
|
|
393
406
|
if uri.scheme == "unix"
|
394
407
|
defaults[:path] = uri.path
|
395
|
-
elsif uri.scheme == "redis"
|
408
|
+
elsif uri.scheme == "redis" || uri.scheme == "rediss"
|
396
409
|
defaults[:scheme] = uri.scheme
|
397
410
|
defaults[:host] = uri.host if uri.host
|
398
411
|
defaults[:port] = uri.port if uri.port
|
@@ -402,6 +415,8 @@ class Redis
|
|
402
415
|
else
|
403
416
|
raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
|
404
417
|
end
|
418
|
+
|
419
|
+
defaults[:ssl] = true if uri.scheme == "rediss"
|
405
420
|
end
|
406
421
|
|
407
422
|
# Use default when option is not specified or nil
|
@@ -420,13 +435,16 @@ class Redis
|
|
420
435
|
options[:port] = options[:port].to_i
|
421
436
|
end
|
422
437
|
|
423
|
-
|
424
|
-
|
425
|
-
options[:
|
426
|
-
|
427
|
-
options[:timeout]
|
438
|
+
if options.has_key?(:timeout)
|
439
|
+
options[:connect_timeout] ||= options[:timeout]
|
440
|
+
options[:read_timeout] ||= options[:timeout]
|
441
|
+
options[:write_timeout] ||= options[:timeout]
|
428
442
|
end
|
429
443
|
|
444
|
+
options[:connect_timeout] = Float(options[:connect_timeout])
|
445
|
+
options[:read_timeout] = Float(options[:read_timeout])
|
446
|
+
options[:write_timeout] = Float(options[:write_timeout])
|
447
|
+
|
430
448
|
options[:db] = options[:db].to_i
|
431
449
|
options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
|
432
450
|
|
@@ -13,12 +13,14 @@ class Redis
|
|
13
13
|
|
14
14
|
if config[:scheme] == "unix"
|
15
15
|
connection.connect_unix(config[:path], connect_timeout)
|
16
|
+
elsif config[:scheme] == "rediss" || config[:ssl]
|
17
|
+
raise NotImplementedError, "SSL not supported by hiredis driver"
|
16
18
|
else
|
17
19
|
connection.connect(config[:host], config[:port], connect_timeout)
|
18
20
|
end
|
19
21
|
|
20
22
|
instance = new(connection)
|
21
|
-
instance.timeout = config[:
|
23
|
+
instance.timeout = config[:read_timeout]
|
22
24
|
instance
|
23
25
|
rescue Errno::ETIMEDOUT
|
24
26
|
raise TimeoutError
|
@@ -2,6 +2,23 @@ require "redis/connection/registry"
|
|
2
2
|
require "redis/connection/command_helper"
|
3
3
|
require "redis/errors"
|
4
4
|
require "socket"
|
5
|
+
require "timeout"
|
6
|
+
|
7
|
+
begin
|
8
|
+
require "openssl"
|
9
|
+
rescue LoadError
|
10
|
+
# Not all systems have OpenSSL support
|
11
|
+
end
|
12
|
+
|
13
|
+
if RUBY_VERSION < "1.9.3"
|
14
|
+
class String
|
15
|
+
# Ruby 1.8.7 does not have byteslice, but it handles encodings differently anyway.
|
16
|
+
# We can simply slice the string, which is a byte array there.
|
17
|
+
def byteslice(*args)
|
18
|
+
slice(*args)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
5
22
|
|
6
23
|
class Redis
|
7
24
|
module Connection
|
@@ -9,10 +26,14 @@ class Redis
|
|
9
26
|
|
10
27
|
CRLF = "\r\n".freeze
|
11
28
|
|
29
|
+
# Exceptions raised during non-blocking I/O ops that require retrying the op
|
30
|
+
NBIO_EXCEPTIONS = [Errno::EWOULDBLOCK, Errno::EAGAIN]
|
31
|
+
NBIO_EXCEPTIONS << IO::WaitReadable if RUBY_VERSION >= "1.9.3"
|
32
|
+
|
12
33
|
def initialize(*args)
|
13
34
|
super(*args)
|
14
35
|
|
15
|
-
@timeout = nil
|
36
|
+
@timeout = @write_timeout = nil
|
16
37
|
@buffer = ""
|
17
38
|
end
|
18
39
|
|
@@ -24,6 +45,14 @@ class Redis
|
|
24
45
|
end
|
25
46
|
end
|
26
47
|
|
48
|
+
def write_timeout=(timeout)
|
49
|
+
if timeout && timeout > 0
|
50
|
+
@write_timeout = timeout
|
51
|
+
else
|
52
|
+
@write_timeout = nil
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
27
56
|
def read(nbytes)
|
28
57
|
result = @buffer.slice!(0, nbytes)
|
29
58
|
|
@@ -45,10 +74,11 @@ class Redis
|
|
45
74
|
end
|
46
75
|
|
47
76
|
def _read_from_socket(nbytes)
|
77
|
+
|
48
78
|
begin
|
49
79
|
read_nonblock(nbytes)
|
50
80
|
|
51
|
-
rescue
|
81
|
+
rescue *NBIO_EXCEPTIONS
|
52
82
|
if IO.select([self], nil, nil, @timeout)
|
53
83
|
retry
|
54
84
|
else
|
@@ -59,6 +89,36 @@ class Redis
|
|
59
89
|
rescue EOFError
|
60
90
|
raise Errno::ECONNRESET
|
61
91
|
end
|
92
|
+
|
93
|
+
def _write_to_socket(data)
|
94
|
+
begin
|
95
|
+
write_nonblock(data)
|
96
|
+
|
97
|
+
rescue *NBIO_EXCEPTIONS
|
98
|
+
if IO.select(nil, [self], nil, @write_timeout)
|
99
|
+
retry
|
100
|
+
else
|
101
|
+
raise Redis::TimeoutError
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
rescue EOFError
|
106
|
+
raise Errno::ECONNRESET
|
107
|
+
end
|
108
|
+
|
109
|
+
def write(data)
|
110
|
+
return super(data) unless @write_timeout
|
111
|
+
|
112
|
+
length = data.bytesize
|
113
|
+
total_count = 0
|
114
|
+
loop do
|
115
|
+
count = _write_to_socket(data)
|
116
|
+
|
117
|
+
total_count += count
|
118
|
+
return total_count if total_count >= length
|
119
|
+
data = data.byteslice(count..-1)
|
120
|
+
end
|
121
|
+
end
|
62
122
|
end
|
63
123
|
|
64
124
|
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
|
@@ -195,6 +255,27 @@ class Redis
|
|
195
255
|
|
196
256
|
end
|
197
257
|
|
258
|
+
if defined?(OpenSSL)
|
259
|
+
class SSLSocket < ::OpenSSL::SSL::SSLSocket
|
260
|
+
include SocketMixin
|
261
|
+
|
262
|
+
def self.connect(host, port, timeout, ssl_params)
|
263
|
+
# Note: this is using Redis::Connection::TCPSocket
|
264
|
+
tcp_sock = TCPSocket.connect(host, port, timeout)
|
265
|
+
|
266
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
267
|
+
ctx.set_params(ssl_params) if ssl_params && !ssl_params.empty?
|
268
|
+
|
269
|
+
ssl_sock = new(tcp_sock, ctx)
|
270
|
+
ssl_sock.hostname = host
|
271
|
+
ssl_sock.connect
|
272
|
+
ssl_sock.post_connection_check(host)
|
273
|
+
|
274
|
+
ssl_sock
|
275
|
+
end
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
198
279
|
class Ruby
|
199
280
|
include Redis::Connection::CommandHelper
|
200
281
|
|
@@ -206,13 +287,17 @@ class Redis
|
|
206
287
|
|
207
288
|
def self.connect(config)
|
208
289
|
if config[:scheme] == "unix"
|
290
|
+
raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl]
|
209
291
|
sock = UNIXSocket.connect(config[:path], config[:connect_timeout])
|
292
|
+
elsif config[:scheme] == "rediss" || config[:ssl]
|
293
|
+
sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params])
|
210
294
|
else
|
211
295
|
sock = TCPSocket.connect(config[:host], config[:port], config[:connect_timeout])
|
212
296
|
end
|
213
297
|
|
214
298
|
instance = new(sock)
|
215
299
|
instance.timeout = config[:timeout]
|
300
|
+
instance.write_timeout = config[:write_timeout]
|
216
301
|
instance.set_tcp_keepalive config[:tcp_keepalive]
|
217
302
|
instance
|
218
303
|
end
|
@@ -265,6 +350,10 @@ class Redis
|
|
265
350
|
end
|
266
351
|
end
|
267
352
|
|
353
|
+
def write_timeout=(timeout)
|
354
|
+
@sock.write_timeout = timeout
|
355
|
+
end
|
356
|
+
|
268
357
|
def write(command)
|
269
358
|
@sock.write(build_command(command))
|
270
359
|
end
|
@@ -9,6 +9,8 @@ class Redis
|
|
9
9
|
class RedisClient < EventMachine::Connection
|
10
10
|
include EventMachine::Deferrable
|
11
11
|
|
12
|
+
attr_accessor :timeout
|
13
|
+
|
12
14
|
def post_init
|
13
15
|
@req = nil
|
14
16
|
@connected = false
|
@@ -44,6 +46,9 @@ class Redis
|
|
44
46
|
|
45
47
|
def read
|
46
48
|
@req = EventMachine::DefaultDeferrable.new
|
49
|
+
if @timeout > 0
|
50
|
+
@req.timeout(@timeout, :timeout)
|
51
|
+
end
|
47
52
|
EventMachine::Synchrony.sync @req
|
48
53
|
end
|
49
54
|
|
@@ -68,6 +73,8 @@ class Redis
|
|
68
73
|
def self.connect(config)
|
69
74
|
if config[:scheme] == "unix"
|
70
75
|
conn = EventMachine.connect_unix_domain(config[:path], RedisClient)
|
76
|
+
elsif config[:scheme] == "rediss" || config[:ssl]
|
77
|
+
raise NotImplementedError, "SSL not supported by synchrony driver"
|
71
78
|
else
|
72
79
|
conn = EventMachine.connect(config[:host], config[:port], RedisClient) do |c|
|
73
80
|
c.pending_connect_timeout = [config[:connect_timeout], 0.1].max
|
@@ -81,7 +88,7 @@ class Redis
|
|
81
88
|
raise Errno::ECONNREFUSED if Fiber.yield == :refused
|
82
89
|
|
83
90
|
instance = new(conn)
|
84
|
-
instance.timeout = config[:
|
91
|
+
instance.timeout = config[:read_timeout]
|
85
92
|
instance
|
86
93
|
end
|
87
94
|
|
@@ -94,7 +101,7 @@ class Redis
|
|
94
101
|
end
|
95
102
|
|
96
103
|
def timeout=(timeout)
|
97
|
-
@timeout = timeout
|
104
|
+
@connection.timeout = timeout
|
98
105
|
end
|
99
106
|
|
100
107
|
def disconnect
|
@@ -113,6 +120,8 @@ class Redis
|
|
113
120
|
payload
|
114
121
|
elsif type == :error
|
115
122
|
raise payload
|
123
|
+
elsif type == :timeout
|
124
|
+
raise TimeoutError
|
116
125
|
else
|
117
126
|
raise "Unknown type #{type.inspect}"
|
118
127
|
end
|
data/lib/redis/subscribe.rb
CHANGED
@@ -12,10 +12,18 @@ class Redis
|
|
12
12
|
subscription("subscribe", "unsubscribe", channels, block)
|
13
13
|
end
|
14
14
|
|
15
|
+
def subscribe_with_timeout(timeout, *channels, &block)
|
16
|
+
subscription("subscribe", "unsubscribe", channels, block, timeout)
|
17
|
+
end
|
18
|
+
|
15
19
|
def psubscribe(*channels, &block)
|
16
20
|
subscription("psubscribe", "punsubscribe", channels, block)
|
17
21
|
end
|
18
22
|
|
23
|
+
def psubscribe_with_timeout(timeout, *channels, &block)
|
24
|
+
subscription("psubscribe", "punsubscribe", channels, block, timeout)
|
25
|
+
end
|
26
|
+
|
19
27
|
def unsubscribe(*channels)
|
20
28
|
call([:unsubscribe, *channels])
|
21
29
|
end
|
@@ -26,13 +34,13 @@ class Redis
|
|
26
34
|
|
27
35
|
protected
|
28
36
|
|
29
|
-
def subscription(start, stop, channels, block)
|
37
|
+
def subscription(start, stop, channels, block, timeout = 0)
|
30
38
|
sub = Subscription.new(&block)
|
31
39
|
|
32
40
|
unsubscribed = false
|
33
41
|
|
34
42
|
begin
|
35
|
-
@client.call_loop([start, *channels]) do |line|
|
43
|
+
@client.call_loop([start, *channels], timeout) do |line|
|
36
44
|
type, *rest = line
|
37
45
|
sub.callbacks[type].call(*rest)
|
38
46
|
unsubscribed = type == stop && rest.last == 0
|
data/lib/redis/version.rb
CHANGED
data/redis.gemspec
CHANGED
@@ -39,6 +39,6 @@ Gem::Specification.new do |s|
|
|
39
39
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
40
40
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
41
41
|
|
42
|
-
s.add_development_dependency("rake")
|
43
|
-
s.add_development_dependency("test-unit")
|
42
|
+
s.add_development_dependency("rake", "<11.0.0")
|
43
|
+
s.add_development_dependency("test-unit", "3.1.5")
|
44
44
|
end
|
data/test/client_test.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
require File.expand_path("helper", File.dirname(__FILE__))
|
2
|
+
|
3
|
+
class TestClient < Test::Unit::TestCase
|
4
|
+
|
5
|
+
include Helper::Client
|
6
|
+
|
7
|
+
def test_call
|
8
|
+
result = r.call("PING")
|
9
|
+
assert_equal result, "PONG"
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_call_with_arguments
|
13
|
+
result = r.call("SET", "foo", "bar")
|
14
|
+
assert_equal result, "OK"
|
15
|
+
end
|
16
|
+
|
17
|
+
def test_call_integers
|
18
|
+
result = r.call("INCR", "foo")
|
19
|
+
assert_equal result, 1
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_call_raise
|
23
|
+
assert_raises(Redis::CommandError) do
|
24
|
+
r.call("INCR")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_queue_commit
|
29
|
+
r.queue("SET", "foo", "bar")
|
30
|
+
r.queue("GET", "foo")
|
31
|
+
result = r.commit
|
32
|
+
|
33
|
+
assert_equal result, ["OK", "bar"]
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_commit_raise
|
37
|
+
r.queue("SET", "foo", "bar")
|
38
|
+
r.queue("INCR")
|
39
|
+
|
40
|
+
assert_raise(Redis::CommandError) do
|
41
|
+
r.commit
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def test_queue_after_error
|
46
|
+
r.queue("SET", "foo", "bar")
|
47
|
+
r.queue("INCR")
|
48
|
+
|
49
|
+
assert_raise(Redis::CommandError) do
|
50
|
+
r.commit
|
51
|
+
end
|
52
|
+
|
53
|
+
r.queue("SET", "foo", "bar")
|
54
|
+
r.queue("INCR", "baz")
|
55
|
+
result = r.commit
|
56
|
+
|
57
|
+
assert_equal result, ["OK", 1]
|
58
|
+
end
|
59
|
+
end
|