redis 3.2.2 → 3.3.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of redis might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/.travis.yml +9 -3
- data/CHANGELOG.md +14 -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 +56 -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/lint/blocking_commands.rb +1 -1
- data/test/lint/sorted_sets.rb +4 -4
- data/test/publish_subscribe_test.rb +28 -0
- 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 +39 -17
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,13 @@ 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
|
5
12
|
|
6
13
|
class Redis
|
7
14
|
module Connection
|
@@ -9,10 +16,14 @@ class Redis
|
|
9
16
|
|
10
17
|
CRLF = "\r\n".freeze
|
11
18
|
|
19
|
+
# Exceptions raised during non-blocking I/O ops that require retrying the op
|
20
|
+
NBIO_EXCEPTIONS = [Errno::EWOULDBLOCK, Errno::EAGAIN]
|
21
|
+
NBIO_EXCEPTIONS << IO::WaitReadable if RUBY_VERSION >= "1.9.3"
|
22
|
+
|
12
23
|
def initialize(*args)
|
13
24
|
super(*args)
|
14
25
|
|
15
|
-
@timeout = nil
|
26
|
+
@timeout = @write_timeout = nil
|
16
27
|
@buffer = ""
|
17
28
|
end
|
18
29
|
|
@@ -24,6 +35,14 @@ class Redis
|
|
24
35
|
end
|
25
36
|
end
|
26
37
|
|
38
|
+
def write_timeout=(timeout)
|
39
|
+
if timeout && timeout > 0
|
40
|
+
@write_timeout = timeout
|
41
|
+
else
|
42
|
+
@write_timeout = nil
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
27
46
|
def read(nbytes)
|
28
47
|
result = @buffer.slice!(0, nbytes)
|
29
48
|
|
@@ -45,10 +64,11 @@ class Redis
|
|
45
64
|
end
|
46
65
|
|
47
66
|
def _read_from_socket(nbytes)
|
67
|
+
|
48
68
|
begin
|
49
69
|
read_nonblock(nbytes)
|
50
70
|
|
51
|
-
rescue
|
71
|
+
rescue *NBIO_EXCEPTIONS
|
52
72
|
if IO.select([self], nil, nil, @timeout)
|
53
73
|
retry
|
54
74
|
else
|
@@ -59,6 +79,11 @@ class Redis
|
|
59
79
|
rescue EOFError
|
60
80
|
raise Errno::ECONNRESET
|
61
81
|
end
|
82
|
+
|
83
|
+
# UNIXSocket and TCPSocket don't support write timeouts
|
84
|
+
def write(*args)
|
85
|
+
Timeout.timeout(@write_timeout, TimeoutError) { super }
|
86
|
+
end
|
62
87
|
end
|
63
88
|
|
64
89
|
if defined?(RUBY_ENGINE) && RUBY_ENGINE == "jruby"
|
@@ -195,6 +220,27 @@ class Redis
|
|
195
220
|
|
196
221
|
end
|
197
222
|
|
223
|
+
if defined?(OpenSSL)
|
224
|
+
class SSLSocket < ::OpenSSL::SSL::SSLSocket
|
225
|
+
include SocketMixin
|
226
|
+
|
227
|
+
def self.connect(host, port, timeout, ssl_params)
|
228
|
+
# Note: this is using Redis::Connection::TCPSocket
|
229
|
+
tcp_sock = TCPSocket.connect(host, port, timeout)
|
230
|
+
|
231
|
+
ctx = OpenSSL::SSL::SSLContext.new
|
232
|
+
ctx.set_params(ssl_params) if ssl_params && !ssl_params.empty?
|
233
|
+
|
234
|
+
ssl_sock = new(tcp_sock, ctx)
|
235
|
+
ssl_sock.hostname = host
|
236
|
+
ssl_sock.connect
|
237
|
+
ssl_sock.post_connection_check(host)
|
238
|
+
|
239
|
+
ssl_sock
|
240
|
+
end
|
241
|
+
end
|
242
|
+
end
|
243
|
+
|
198
244
|
class Ruby
|
199
245
|
include Redis::Connection::CommandHelper
|
200
246
|
|
@@ -206,13 +252,17 @@ class Redis
|
|
206
252
|
|
207
253
|
def self.connect(config)
|
208
254
|
if config[:scheme] == "unix"
|
255
|
+
raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl]
|
209
256
|
sock = UNIXSocket.connect(config[:path], config[:connect_timeout])
|
257
|
+
elsif config[:scheme] == "rediss" || config[:ssl]
|
258
|
+
sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params])
|
210
259
|
else
|
211
260
|
sock = TCPSocket.connect(config[:host], config[:port], config[:connect_timeout])
|
212
261
|
end
|
213
262
|
|
214
263
|
instance = new(sock)
|
215
264
|
instance.timeout = config[:timeout]
|
265
|
+
instance.write_timeout = config[:write_timeout]
|
216
266
|
instance.set_tcp_keepalive config[:tcp_keepalive]
|
217
267
|
instance
|
218
268
|
end
|
@@ -265,6 +315,10 @@ class Redis
|
|
265
315
|
end
|
266
316
|
end
|
267
317
|
|
318
|
+
def write_timeout=(timeout)
|
319
|
+
@sock.write_timeout = timeout
|
320
|
+
end
|
321
|
+
|
268
322
|
def write(command)
|
269
323
|
@sock.write(build_command(command))
|
270
324
|
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
|
@@ -51,6 +51,33 @@ class TestConnectionHandling < Test::Unit::TestCase
|
|
51
51
|
assert !r.client.connected?
|
52
52
|
end
|
53
53
|
|
54
|
+
def test_close
|
55
|
+
quit = 0
|
56
|
+
|
57
|
+
commands = {
|
58
|
+
:quit => lambda do
|
59
|
+
quit += 1
|
60
|
+
"+OK"
|
61
|
+
end
|
62
|
+
}
|
63
|
+
|
64
|
+
redis_mock(commands) do |redis|
|
65
|
+
assert_equal 0, quit
|
66
|
+
|
67
|
+
redis.quit
|
68
|
+
|
69
|
+
assert_equal 1, quit
|
70
|
+
|
71
|
+
redis.ping
|
72
|
+
|
73
|
+
redis.close
|
74
|
+
|
75
|
+
assert_equal 1, quit
|
76
|
+
|
77
|
+
assert !redis.connected?
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
54
81
|
def test_disconnect
|
55
82
|
quit = 0
|
56
83
|
|