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.

@@ -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[:timeout]
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, &block)
119
+ def call(command)
113
120
  reply = process([command]) { read }
114
121
  raise reply if reply.is_a?(CommandError)
115
122
 
116
- if block
117
- block.call(reply)
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 = without_socket_timeout do
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
- result[i + 1] = read
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
- options[:timeout] = options[:timeout].to_f
424
- options[:connect_timeout] = if options[:connect_timeout]
425
- options[:connect_timeout].to_f
426
- else
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[:timeout]
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 Errno::EWOULDBLOCK, Errno::EAGAIN
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[:timeout]
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  class Redis
2
- VERSION = "3.2.2"
2
+ VERSION = "3.3.0"
3
3
  end
@@ -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
@@ -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