redis 3.2.2 → 4.4.0

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.
Files changed (118) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +175 -13
  3. data/README.md +223 -76
  4. data/lib/redis.rb +1360 -445
  5. data/lib/redis/client.rb +183 -103
  6. data/lib/redis/cluster.rb +291 -0
  7. data/lib/redis/cluster/command.rb +81 -0
  8. data/lib/redis/cluster/command_loader.rb +34 -0
  9. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  10. data/lib/redis/cluster/node.rb +108 -0
  11. data/lib/redis/cluster/node_key.rb +31 -0
  12. data/lib/redis/cluster/node_loader.rb +37 -0
  13. data/lib/redis/cluster/option.rb +93 -0
  14. data/lib/redis/cluster/slot.rb +86 -0
  15. data/lib/redis/cluster/slot_loader.rb +49 -0
  16. data/lib/redis/connection.rb +4 -2
  17. data/lib/redis/connection/command_helper.rb +5 -10
  18. data/lib/redis/connection/hiredis.rb +9 -6
  19. data/lib/redis/connection/registry.rb +2 -1
  20. data/lib/redis/connection/ruby.rb +168 -63
  21. data/lib/redis/connection/synchrony.rb +29 -7
  22. data/lib/redis/distributed.rb +156 -74
  23. data/lib/redis/errors.rb +48 -0
  24. data/lib/redis/hash_ring.rb +30 -73
  25. data/lib/redis/pipeline.rb +55 -15
  26. data/lib/redis/subscribe.rb +20 -13
  27. data/lib/redis/version.rb +3 -1
  28. metadata +41 -170
  29. data/.gitignore +0 -16
  30. data/.travis.yml +0 -59
  31. data/.travis/Gemfile +0 -11
  32. data/.yardopts +0 -3
  33. data/Gemfile +0 -4
  34. data/Rakefile +0 -87
  35. data/benchmarking/logging.rb +0 -71
  36. data/benchmarking/pipeline.rb +0 -51
  37. data/benchmarking/speed.rb +0 -21
  38. data/benchmarking/suite.rb +0 -24
  39. data/benchmarking/worker.rb +0 -71
  40. data/examples/basic.rb +0 -15
  41. data/examples/consistency.rb +0 -114
  42. data/examples/dist_redis.rb +0 -43
  43. data/examples/incr-decr.rb +0 -17
  44. data/examples/list.rb +0 -26
  45. data/examples/pubsub.rb +0 -37
  46. data/examples/sentinel.rb +0 -41
  47. data/examples/sentinel/sentinel.conf +0 -9
  48. data/examples/sentinel/start +0 -49
  49. data/examples/sets.rb +0 -36
  50. data/examples/unicorn/config.ru +0 -3
  51. data/examples/unicorn/unicorn.rb +0 -20
  52. data/redis.gemspec +0 -44
  53. data/test/bitpos_test.rb +0 -69
  54. data/test/blocking_commands_test.rb +0 -42
  55. data/test/command_map_test.rb +0 -30
  56. data/test/commands_on_hashes_test.rb +0 -21
  57. data/test/commands_on_hyper_log_log_test.rb +0 -21
  58. data/test/commands_on_lists_test.rb +0 -20
  59. data/test/commands_on_sets_test.rb +0 -77
  60. data/test/commands_on_sorted_sets_test.rb +0 -137
  61. data/test/commands_on_strings_test.rb +0 -101
  62. data/test/commands_on_value_types_test.rb +0 -133
  63. data/test/connection_handling_test.rb +0 -250
  64. data/test/db/.gitkeep +0 -0
  65. data/test/distributed_blocking_commands_test.rb +0 -46
  66. data/test/distributed_commands_on_hashes_test.rb +0 -10
  67. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  68. data/test/distributed_commands_on_lists_test.rb +0 -22
  69. data/test/distributed_commands_on_sets_test.rb +0 -83
  70. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  71. data/test/distributed_commands_on_strings_test.rb +0 -59
  72. data/test/distributed_commands_on_value_types_test.rb +0 -95
  73. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  74. data/test/distributed_connection_handling_test.rb +0 -23
  75. data/test/distributed_internals_test.rb +0 -79
  76. data/test/distributed_key_tags_test.rb +0 -52
  77. data/test/distributed_persistence_control_commands_test.rb +0 -26
  78. data/test/distributed_publish_subscribe_test.rb +0 -92
  79. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  80. data/test/distributed_scripting_test.rb +0 -102
  81. data/test/distributed_sorting_test.rb +0 -20
  82. data/test/distributed_test.rb +0 -58
  83. data/test/distributed_transactions_test.rb +0 -32
  84. data/test/encoding_test.rb +0 -18
  85. data/test/error_replies_test.rb +0 -59
  86. data/test/fork_safety_test.rb +0 -65
  87. data/test/helper.rb +0 -232
  88. data/test/helper_test.rb +0 -24
  89. data/test/internals_test.rb +0 -437
  90. data/test/lint/blocking_commands.rb +0 -150
  91. data/test/lint/hashes.rb +0 -162
  92. data/test/lint/hyper_log_log.rb +0 -60
  93. data/test/lint/lists.rb +0 -143
  94. data/test/lint/sets.rb +0 -125
  95. data/test/lint/sorted_sets.rb +0 -316
  96. data/test/lint/strings.rb +0 -260
  97. data/test/lint/value_types.rb +0 -122
  98. data/test/persistence_control_commands_test.rb +0 -26
  99. data/test/pipelining_commands_test.rb +0 -242
  100. data/test/publish_subscribe_test.rb +0 -254
  101. data/test/remote_server_control_commands_test.rb +0 -118
  102. data/test/scanning_test.rb +0 -413
  103. data/test/scripting_test.rb +0 -78
  104. data/test/sentinel_command_test.rb +0 -80
  105. data/test/sentinel_test.rb +0 -255
  106. data/test/sorting_test.rb +0 -59
  107. data/test/support/connection/hiredis.rb +0 -1
  108. data/test/support/connection/ruby.rb +0 -1
  109. data/test/support/connection/synchrony.rb +0 -17
  110. data/test/support/redis_mock.rb +0 -119
  111. data/test/support/wire/synchrony.rb +0 -24
  112. data/test/support/wire/thread.rb +0 -5
  113. data/test/synchrony_driver.rb +0 -88
  114. data/test/test.conf.erb +0 -9
  115. data/test/thread_safety_test.rb +0 -32
  116. data/test/transactions_test.rb +0 -264
  117. data/test/unknown_commands_test.rb +0 -14
  118. data/test/url_param_test.rb +0 -138
@@ -1,24 +1,27 @@
1
- require "redis/connection/registry"
2
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+ require_relative "../errors"
3
5
  require "hiredis/connection"
4
6
  require "timeout"
5
7
 
6
8
  class Redis
7
9
  module Connection
8
10
  class Hiredis
9
-
10
11
  def self.connect(config)
11
12
  connection = ::Hiredis::Connection.new
12
13
  connect_timeout = (config.fetch(:connect_timeout, 0) * 1_000_000).to_i
13
14
 
14
15
  if config[:scheme] == "unix"
15
16
  connection.connect_unix(config[:path], connect_timeout)
17
+ elsif config[:scheme] == "rediss" || config[:ssl]
18
+ raise NotImplementedError, "SSL not supported by hiredis driver"
16
19
  else
17
20
  connection.connect(config[:host], config[:port], connect_timeout)
18
21
  end
19
22
 
20
23
  instance = new(connection)
21
- instance.timeout = config[:timeout]
24
+ instance.timeout = config[:read_timeout]
22
25
  instance
23
26
  rescue Errno::ETIMEDOUT
24
27
  raise TimeoutError
@@ -29,7 +32,7 @@ class Redis
29
32
  end
30
33
 
31
34
  def connected?
32
- @connection && @connection.connected?
35
+ @connection&.connected?
33
36
  end
34
37
 
35
38
  def timeout=(timeout)
@@ -55,7 +58,7 @@ class Redis
55
58
  rescue Errno::EAGAIN
56
59
  raise TimeoutError
57
60
  rescue RuntimeError => err
58
- raise ProtocolError.new(err.message)
61
+ raise ProtocolError, err.message
59
62
  end
60
63
  end
61
64
  end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  class Redis
2
4
  module Connection
3
-
4
5
  # Store a list of loaded connection drivers in the Connection module.
5
6
  # Redis::Client uses the last required driver by default, and will be aware
6
7
  # of the loaded connection drivers if the user chooses to override the
@@ -1,63 +1,99 @@
1
- require "redis/connection/registry"
2
- require "redis/connection/command_helper"
3
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "registry"
4
+ require_relative "command_helper"
5
+ require_relative "../errors"
4
6
  require "socket"
7
+ require "timeout"
8
+
9
+ begin
10
+ require "openssl"
11
+ rescue LoadError
12
+ # Not all systems have OpenSSL support
13
+ end
5
14
 
6
15
  class Redis
7
16
  module Connection
8
17
  module SocketMixin
9
-
10
- CRLF = "\r\n".freeze
18
+ CRLF = "\r\n"
11
19
 
12
20
  def initialize(*args)
13
21
  super(*args)
14
22
 
15
- @timeout = nil
16
- @buffer = ""
23
+ @timeout = @write_timeout = nil
24
+ @buffer = "".dup
17
25
  end
18
26
 
19
27
  def timeout=(timeout)
20
- if timeout && timeout > 0
21
- @timeout = timeout
22
- else
23
- @timeout = nil
24
- end
28
+ @timeout = (timeout if timeout && timeout > 0)
29
+ end
30
+
31
+ def write_timeout=(timeout)
32
+ @write_timeout = (timeout if timeout && timeout > 0)
25
33
  end
26
34
 
27
35
  def read(nbytes)
28
36
  result = @buffer.slice!(0, nbytes)
29
37
 
30
- while result.bytesize < nbytes
31
- result << _read_from_socket(nbytes - result.bytesize)
32
- end
38
+ result << _read_from_socket(nbytes - result.bytesize) while result.bytesize < nbytes
33
39
 
34
40
  result
35
41
  end
36
42
 
37
43
  def gets
38
- crlf = nil
39
-
40
- while (crlf = @buffer.index(CRLF)) == nil
41
- @buffer << _read_from_socket(1024)
44
+ while (crlf = @buffer.index(CRLF)).nil?
45
+ @buffer << _read_from_socket(16_384)
42
46
  end
43
47
 
44
48
  @buffer.slice!(0, crlf + CRLF.bytesize)
45
49
  end
46
50
 
47
51
  def _read_from_socket(nbytes)
48
- begin
49
- read_nonblock(nbytes)
50
-
51
- rescue Errno::EWOULDBLOCK, Errno::EAGAIN
52
- if IO.select([self], nil, nil, @timeout)
53
- retry
54
- else
55
- raise Redis::TimeoutError
52
+ loop do
53
+ case chunk = read_nonblock(nbytes, exception: false)
54
+ when :wait_readable
55
+ unless wait_readable(@timeout)
56
+ raise Redis::TimeoutError
57
+ end
58
+ when :wait_writable
59
+ unless wait_writable(@timeout)
60
+ raise Redis::TimeoutError
61
+ end
62
+ when nil
63
+ raise Errno::ECONNRESET
64
+ when String
65
+ return chunk
56
66
  end
57
67
  end
68
+ end
58
69
 
59
- rescue EOFError
60
- raise Errno::ECONNRESET
70
+ def write(buffer)
71
+ return super(buffer) unless @write_timeout
72
+
73
+ bytes_to_write = buffer.bytesize
74
+ total_bytes_written = 0
75
+ loop do
76
+ case bytes_written = write_nonblock(buffer, exception: false)
77
+ when :wait_readable
78
+ unless wait_readable(@write_timeout)
79
+ raise Redis::TimeoutError
80
+ end
81
+ when :wait_writable
82
+ unless wait_writable(@write_timeout)
83
+ raise Redis::TimeoutError
84
+ end
85
+ when nil
86
+ raise Errno::ECONNRESET
87
+ when Integer
88
+ total_bytes_written += bytes_written
89
+
90
+ if total_bytes_written >= bytes_to_write
91
+ return total_bytes_written
92
+ end
93
+
94
+ buffer = buffer.byteslice(bytes_written..-1)
95
+ end
96
+ end
61
97
  end
62
98
  end
63
99
 
@@ -66,7 +102,6 @@ class Redis
66
102
  require "timeout"
67
103
 
68
104
  class TCPSocket < ::TCPSocket
69
-
70
105
  include SocketMixin
71
106
 
72
107
  def self.connect(host, port, timeout)
@@ -82,7 +117,6 @@ class Redis
82
117
  if defined?(::UNIXSocket)
83
118
 
84
119
  class UNIXSocket < ::UNIXSocket
85
-
86
120
  include SocketMixin
87
121
 
88
122
  def self.connect(path, timeout)
@@ -94,13 +128,12 @@ class Redis
94
128
  raise TimeoutError
95
129
  end
96
130
 
97
- # JRuby raises Errno::EAGAIN on #read_nonblock even when IO.select
131
+ # JRuby raises Errno::EAGAIN on #read_nonblock even when it
98
132
  # says it is readable (1.6.6, in both 1.8 and 1.9 mode).
99
133
  # Use the blocking #readpartial method instead.
100
134
 
101
135
  def _read_from_socket(nbytes)
102
136
  readpartial(nbytes)
103
-
104
137
  rescue EOFError
105
138
  raise Errno::ECONNRESET
106
139
  end
@@ -111,19 +144,16 @@ class Redis
111
144
  else
112
145
 
113
146
  class TCPSocket < ::Socket
114
-
115
147
  include SocketMixin
116
148
 
117
- def self.connect_addrinfo(ai, port, timeout)
118
- sock = new(::Socket.const_get(ai[0]), Socket::SOCK_STREAM, 0)
119
- sockaddr = ::Socket.pack_sockaddr_in(port, ai[3])
149
+ def self.connect_addrinfo(addrinfo, port, timeout)
150
+ sock = new(::Socket.const_get(addrinfo[0]), Socket::SOCK_STREAM, 0)
151
+ sockaddr = ::Socket.pack_sockaddr_in(port, addrinfo[3])
120
152
 
121
153
  begin
122
154
  sock.connect_nonblock(sockaddr)
123
155
  rescue Errno::EINPROGRESS
124
- if IO.select(nil, [sock], nil, timeout) == nil
125
- raise TimeoutError
126
- end
156
+ raise TimeoutError unless sock.wait_writable(timeout)
127
157
 
128
158
  begin
129
159
  sock.connect_nonblock(sockaddr)
@@ -162,14 +192,13 @@ class Redis
162
192
  return connect_addrinfo(ai, port, timeout)
163
193
  rescue SystemCallError
164
194
  # Raise if this was our last attempt.
165
- raise if addrinfo.length == i+1
195
+ raise if addrinfo.length == i + 1
166
196
  end
167
197
  end
168
198
  end
169
199
  end
170
200
 
171
201
  class UNIXSocket < ::Socket
172
-
173
202
  include SocketMixin
174
203
 
175
204
  def self.connect(path, timeout)
@@ -179,9 +208,7 @@ class Redis
179
208
  begin
180
209
  sock.connect_nonblock(sockaddr)
181
210
  rescue Errno::EINPROGRESS
182
- if IO.select(nil, [sock], nil, timeout) == nil
183
- raise TimeoutError
184
- end
211
+ raise TimeoutError unless sock.wait_writable(timeout)
185
212
 
186
213
  begin
187
214
  sock.connect_nonblock(sockaddr)
@@ -195,29 +222,97 @@ class Redis
195
222
 
196
223
  end
197
224
 
225
+ if defined?(OpenSSL)
226
+ class SSLSocket < ::OpenSSL::SSL::SSLSocket
227
+ include SocketMixin
228
+
229
+ unless method_defined?(:wait_readable)
230
+ def wait_readable(timeout = nil)
231
+ to_io.wait_readable(timeout)
232
+ end
233
+ end
234
+
235
+ unless method_defined?(:wait_writable)
236
+ def wait_writable(timeout = nil)
237
+ to_io.wait_writable(timeout)
238
+ end
239
+ end
240
+
241
+ def self.connect(host, port, timeout, ssl_params)
242
+ # Note: this is using Redis::Connection::TCPSocket
243
+ tcp_sock = TCPSocket.connect(host, port, timeout)
244
+
245
+ ctx = OpenSSL::SSL::SSLContext.new
246
+
247
+ # The provided parameters are merged into OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
248
+ ctx.set_params(ssl_params || {})
249
+
250
+ ssl_sock = new(tcp_sock, ctx)
251
+ ssl_sock.hostname = host
252
+
253
+ begin
254
+ # Initiate the socket connection in the background. If it doesn't fail
255
+ # immediately it will raise an IO::WaitWritable (Errno::EINPROGRESS)
256
+ # indicating the connection is in progress.
257
+ # Unlike waiting for a tcp socket to connect, you can't time out ssl socket
258
+ # connections during the connect phase properly, because IO.select only partially works.
259
+ # Instead, you have to retry.
260
+ ssl_sock.connect_nonblock
261
+ rescue Errno::EAGAIN, Errno::EWOULDBLOCK, IO::WaitReadable
262
+ if ssl_sock.wait_readable(timeout)
263
+ retry
264
+ else
265
+ raise TimeoutError
266
+ end
267
+ rescue IO::WaitWritable
268
+ if ssl_sock.wait_writable(timeout)
269
+ retry
270
+ else
271
+ raise TimeoutError
272
+ end
273
+ end
274
+
275
+ unless ctx.verify_mode == OpenSSL::SSL::VERIFY_NONE || (
276
+ ctx.respond_to?(:verify_hostname) &&
277
+ !ctx.verify_hostname
278
+ )
279
+ ssl_sock.post_connection_check(host)
280
+ end
281
+
282
+ ssl_sock
283
+ end
284
+ end
285
+ end
286
+
198
287
  class Ruby
199
288
  include Redis::Connection::CommandHelper
200
289
 
201
- MINUS = "-".freeze
202
- PLUS = "+".freeze
203
- COLON = ":".freeze
204
- DOLLAR = "$".freeze
205
- ASTERISK = "*".freeze
290
+ MINUS = "-"
291
+ PLUS = "+"
292
+ COLON = ":"
293
+ DOLLAR = "$"
294
+ ASTERISK = "*"
206
295
 
207
296
  def self.connect(config)
208
297
  if config[:scheme] == "unix"
298
+ raise ArgumentError, "SSL incompatible with unix sockets" if config[:ssl]
299
+
209
300
  sock = UNIXSocket.connect(config[:path], config[:connect_timeout])
301
+ elsif config[:scheme] == "rediss" || config[:ssl]
302
+ sock = SSLSocket.connect(config[:host], config[:port], config[:connect_timeout], config[:ssl_params])
210
303
  else
211
304
  sock = TCPSocket.connect(config[:host], config[:port], config[:connect_timeout])
212
305
  end
213
306
 
214
307
  instance = new(sock)
215
- instance.timeout = config[:timeout]
308
+ instance.timeout = config[:read_timeout]
309
+ instance.write_timeout = config[:write_timeout]
216
310
  instance.set_tcp_keepalive config[:tcp_keepalive]
311
+ instance.set_tcp_nodelay if sock.is_a? TCPSocket
217
312
  instance
218
313
  end
219
314
 
220
- if [:SOL_SOCKET, :SO_KEEPALIVE, :SOL_TCP, :TCP_KEEPIDLE, :TCP_KEEPINTVL, :TCP_KEEPCNT].all?{|c| Socket.const_defined? c}
315
+ if %i[SOL_SOCKET SO_KEEPALIVE SOL_TCP TCP_KEEPIDLE TCP_KEEPINTVL TCP_KEEPCNT].all? { |c| Socket.const_defined? c }
221
316
  def set_tcp_keepalive(keepalive)
222
317
  return unless keepalive.is_a?(Hash)
223
318
 
@@ -229,14 +324,13 @@ class Redis
229
324
 
230
325
  def get_tcp_keepalive
231
326
  {
232
- :time => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int,
233
- :intvl => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int,
234
- :probes => @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int,
327
+ time: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPIDLE).int,
328
+ intvl: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPINTVL).int,
329
+ probes: @sock.getsockopt(Socket::SOL_TCP, Socket::TCP_KEEPCNT).int
235
330
  }
236
331
  end
237
332
  else
238
- def set_tcp_keepalive(keepalive)
239
- end
333
+ def set_tcp_keepalive(keepalive); end
240
334
 
241
335
  def get_tcp_keepalive
242
336
  {
@@ -244,12 +338,21 @@ class Redis
244
338
  end
245
339
  end
246
340
 
341
+ # disables Nagle's Algorithm, prevents multiple round trips with MULTI
342
+ if %i[IPPROTO_TCP TCP_NODELAY].all? { |c| Socket.const_defined? c }
343
+ def set_tcp_nodelay
344
+ @sock.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1)
345
+ end
346
+ else
347
+ def set_tcp_nodelay; end
348
+ end
349
+
247
350
  def initialize(sock)
248
351
  @sock = sock
249
352
  end
250
353
 
251
354
  def connected?
252
- !! @sock
355
+ !!@sock
253
356
  end
254
357
 
255
358
  def disconnect
@@ -260,9 +363,11 @@ class Redis
260
363
  end
261
364
 
262
365
  def timeout=(timeout)
263
- if @sock.respond_to?(:timeout=)
264
- @sock.timeout = timeout
265
- end
366
+ @sock.timeout = timeout if @sock.respond_to?(:timeout=)
367
+ end
368
+
369
+ def write_timeout=(timeout)
370
+ @sock.write_timeout = timeout
266
371
  end
267
372
 
268
373
  def write(command)
@@ -273,7 +378,6 @@ class Redis
273
378
  line = @sock.gets
274
379
  reply_type = line.slice!(0, 1)
275
380
  format_reply(reply_type, line)
276
-
277
381
  rescue Errno::EAGAIN
278
382
  raise TimeoutError
279
383
  end
@@ -285,7 +389,7 @@ class Redis
285
389
  when COLON then format_integer_reply(line)
286
390
  when DOLLAR then format_bulk_reply(line)
287
391
  when ASTERISK then format_multi_bulk_reply(line)
288
- else raise ProtocolError.new(reply_type)
392
+ else raise ProtocolError, reply_type
289
393
  end
290
394
  end
291
395
 
@@ -304,6 +408,7 @@ class Redis
304
408
  def format_bulk_reply(line)
305
409
  bulklen = line.to_i
306
410
  return if bulklen == -1
411
+
307
412
  reply = encode(@sock.read(bulklen))
308
413
  @sock.read(2) # Discard CRLF.
309
414
  reply