redis 3.2.2 → 3.3.1

Sign up to get free protection for your applications and to get access to all the features.
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[: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,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 Errno::EWOULDBLOCK, Errno::EAGAIN
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[: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
data/lib/redis/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  class Redis
2
- VERSION = "3.2.2"
2
+ VERSION = "3.3.1"
3
3
  end
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
@@ -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