redis 3.2.2 → 3.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|