redis 3.2.0 → 4.6.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 (133) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +278 -15
  3. data/README.md +260 -76
  4. data/lib/redis/client.rb +239 -115
  5. data/lib/redis/cluster/command.rb +79 -0
  6. data/lib/redis/cluster/command_loader.rb +33 -0
  7. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  8. data/lib/redis/cluster/node.rb +120 -0
  9. data/lib/redis/cluster/node_key.rb +31 -0
  10. data/lib/redis/cluster/node_loader.rb +37 -0
  11. data/lib/redis/cluster/option.rb +93 -0
  12. data/lib/redis/cluster/slot.rb +86 -0
  13. data/lib/redis/cluster/slot_loader.rb +49 -0
  14. data/lib/redis/cluster.rb +315 -0
  15. data/lib/redis/commands/bitmaps.rb +63 -0
  16. data/lib/redis/commands/cluster.rb +45 -0
  17. data/lib/redis/commands/connection.rb +58 -0
  18. data/lib/redis/commands/geo.rb +84 -0
  19. data/lib/redis/commands/hashes.rb +251 -0
  20. data/lib/redis/commands/hyper_log_log.rb +37 -0
  21. data/lib/redis/commands/keys.rb +411 -0
  22. data/lib/redis/commands/lists.rb +289 -0
  23. data/lib/redis/commands/pubsub.rb +72 -0
  24. data/lib/redis/commands/scripting.rb +114 -0
  25. data/lib/redis/commands/server.rb +188 -0
  26. data/lib/redis/commands/sets.rb +207 -0
  27. data/lib/redis/commands/sorted_sets.rb +804 -0
  28. data/lib/redis/commands/streams.rb +382 -0
  29. data/lib/redis/commands/strings.rb +313 -0
  30. data/lib/redis/commands/transactions.rb +92 -0
  31. data/lib/redis/commands.rb +242 -0
  32. data/lib/redis/connection/command_helper.rb +7 -10
  33. data/lib/redis/connection/hiredis.rb +11 -6
  34. data/lib/redis/connection/registry.rb +2 -1
  35. data/lib/redis/connection/ruby.rb +173 -64
  36. data/lib/redis/connection/synchrony.rb +32 -8
  37. data/lib/redis/connection.rb +3 -1
  38. data/lib/redis/distributed.rb +233 -74
  39. data/lib/redis/errors.rb +48 -0
  40. data/lib/redis/hash_ring.rb +30 -72
  41. data/lib/redis/pipeline.rb +145 -12
  42. data/lib/redis/subscribe.rb +20 -13
  43. data/lib/redis/version.rb +3 -1
  44. data/lib/redis.rb +171 -2476
  45. metadata +71 -165
  46. data/.gitignore +0 -15
  47. data/.travis/Gemfile +0 -11
  48. data/.travis.yml +0 -54
  49. data/.yardopts +0 -3
  50. data/Gemfile +0 -4
  51. data/Rakefile +0 -68
  52. data/benchmarking/logging.rb +0 -71
  53. data/benchmarking/pipeline.rb +0 -51
  54. data/benchmarking/speed.rb +0 -21
  55. data/benchmarking/suite.rb +0 -24
  56. data/benchmarking/worker.rb +0 -71
  57. data/examples/basic.rb +0 -15
  58. data/examples/consistency.rb +0 -114
  59. data/examples/dist_redis.rb +0 -43
  60. data/examples/incr-decr.rb +0 -17
  61. data/examples/list.rb +0 -26
  62. data/examples/pubsub.rb +0 -37
  63. data/examples/sentinel/sentinel.conf +0 -9
  64. data/examples/sentinel/start +0 -49
  65. data/examples/sentinel.rb +0 -41
  66. data/examples/sets.rb +0 -36
  67. data/examples/unicorn/config.ru +0 -3
  68. data/examples/unicorn/unicorn.rb +0 -20
  69. data/redis.gemspec +0 -43
  70. data/test/bitpos_test.rb +0 -69
  71. data/test/blocking_commands_test.rb +0 -42
  72. data/test/command_map_test.rb +0 -30
  73. data/test/commands_on_hashes_test.rb +0 -21
  74. data/test/commands_on_hyper_log_log_test.rb +0 -21
  75. data/test/commands_on_lists_test.rb +0 -20
  76. data/test/commands_on_sets_test.rb +0 -77
  77. data/test/commands_on_sorted_sets_test.rb +0 -123
  78. data/test/commands_on_strings_test.rb +0 -101
  79. data/test/commands_on_value_types_test.rb +0 -131
  80. data/test/connection_handling_test.rb +0 -189
  81. data/test/db/.gitkeep +0 -0
  82. data/test/distributed_blocking_commands_test.rb +0 -46
  83. data/test/distributed_commands_on_hashes_test.rb +0 -10
  84. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  85. data/test/distributed_commands_on_lists_test.rb +0 -22
  86. data/test/distributed_commands_on_sets_test.rb +0 -83
  87. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  88. data/test/distributed_commands_on_strings_test.rb +0 -59
  89. data/test/distributed_commands_on_value_types_test.rb +0 -95
  90. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  91. data/test/distributed_connection_handling_test.rb +0 -23
  92. data/test/distributed_internals_test.rb +0 -70
  93. data/test/distributed_key_tags_test.rb +0 -52
  94. data/test/distributed_persistence_control_commands_test.rb +0 -26
  95. data/test/distributed_publish_subscribe_test.rb +0 -92
  96. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  97. data/test/distributed_scripting_test.rb +0 -102
  98. data/test/distributed_sorting_test.rb +0 -20
  99. data/test/distributed_test.rb +0 -58
  100. data/test/distributed_transactions_test.rb +0 -32
  101. data/test/encoding_test.rb +0 -18
  102. data/test/error_replies_test.rb +0 -59
  103. data/test/fork_safety_test.rb +0 -65
  104. data/test/helper.rb +0 -232
  105. data/test/helper_test.rb +0 -24
  106. data/test/internals_test.rb +0 -434
  107. data/test/lint/blocking_commands.rb +0 -150
  108. data/test/lint/hashes.rb +0 -162
  109. data/test/lint/hyper_log_log.rb +0 -60
  110. data/test/lint/lists.rb +0 -143
  111. data/test/lint/sets.rb +0 -125
  112. data/test/lint/sorted_sets.rb +0 -238
  113. data/test/lint/strings.rb +0 -260
  114. data/test/lint/value_types.rb +0 -122
  115. data/test/persistence_control_commands_test.rb +0 -26
  116. data/test/pipelining_commands_test.rb +0 -242
  117. data/test/publish_subscribe_test.rb +0 -210
  118. data/test/remote_server_control_commands_test.rb +0 -117
  119. data/test/scanning_test.rb +0 -413
  120. data/test/scripting_test.rb +0 -78
  121. data/test/sorting_test.rb +0 -59
  122. data/test/support/connection/hiredis.rb +0 -1
  123. data/test/support/connection/ruby.rb +0 -1
  124. data/test/support/connection/synchrony.rb +0 -17
  125. data/test/support/redis_mock.rb +0 -115
  126. data/test/support/wire/synchrony.rb +0 -24
  127. data/test/support/wire/thread.rb +0 -5
  128. data/test/synchrony_driver.rb +0 -88
  129. data/test/test.conf +0 -9
  130. data/test/thread_safety_test.rb +0 -32
  131. data/test/transactions_test.rb +0 -264
  132. data/test/unknown_commands_test.rb +0 -14
  133. data/test/url_param_test.rb +0 -132
data/lib/redis/client.rb CHANGED
@@ -1,29 +1,38 @@
1
- require "redis/errors"
1
+ # frozen_string_literal: true
2
+
2
3
  require "socket"
3
4
  require "cgi"
5
+ require "redis/errors"
4
6
 
5
7
  class Redis
6
8
  class Client
7
-
9
+ # Defaults are also used for converting string keys to symbols.
8
10
  DEFAULTS = {
9
- :url => lambda { ENV["REDIS_URL"] },
10
- :scheme => "redis",
11
- :host => "127.0.0.1",
12
- :port => 6379,
13
- :path => nil,
14
- :timeout => 5.0,
15
- :password => nil,
16
- :db => 0,
17
- :driver => nil,
18
- :id => nil,
19
- :tcp_keepalive => 0,
20
- :reconnect_attempts => 1,
21
- :inherit_socket => false
22
- }
23
-
24
- def options
25
- Marshal.load(Marshal.dump(@options))
26
- end
11
+ url: -> { ENV["REDIS_URL"] },
12
+ scheme: "redis",
13
+ host: "127.0.0.1",
14
+ port: 6379,
15
+ path: nil,
16
+ read_timeout: nil,
17
+ write_timeout: nil,
18
+ connect_timeout: nil,
19
+ timeout: 5.0,
20
+ username: nil,
21
+ password: nil,
22
+ db: 0,
23
+ driver: nil,
24
+ id: nil,
25
+ tcp_keepalive: 0,
26
+ reconnect_attempts: 1,
27
+ reconnect_delay: 0,
28
+ reconnect_delay_max: 0.5,
29
+ inherit_socket: false,
30
+ logger: nil,
31
+ sentinels: nil,
32
+ role: nil
33
+ }.freeze
34
+
35
+ attr_reader :options, :connection, :command_map
27
36
 
28
37
  def scheme
29
38
  @options[:scheme]
@@ -41,8 +50,20 @@ class Redis
41
50
  @options[:path]
42
51
  end
43
52
 
53
+ def read_timeout
54
+ @options[:read_timeout]
55
+ end
56
+
57
+ def connect_timeout
58
+ @options[:connect_timeout]
59
+ end
60
+
44
61
  def timeout
45
- @options[:timeout]
62
+ @options[:read_timeout]
63
+ end
64
+
65
+ def username
66
+ @options[:username]
46
67
  end
47
68
 
48
69
  def password
@@ -66,8 +87,6 @@ class Redis
66
87
  end
67
88
 
68
89
  attr_accessor :logger
69
- attr_reader :connection
70
- attr_reader :command_map
71
90
 
72
91
  def initialize(options = {})
73
92
  @options = _parse_options(options)
@@ -76,11 +95,16 @@ class Redis
76
95
  @connection = nil
77
96
  @command_map = {}
78
97
 
79
- if options.include?(:sentinels)
80
- @connector = Connector::Sentinel.new(@options)
81
- else
82
- @connector = Connector.new(@options)
83
- end
98
+ @pending_reads = 0
99
+
100
+ @connector =
101
+ if !@options[:sentinels].nil?
102
+ Connector::Sentinel.new(@options)
103
+ elsif options.include?(:connector) && options[:connector].respond_to?(:new)
104
+ options.delete(:connector).new(@options)
105
+ else
106
+ Connector.new(@options)
107
+ end
84
108
  end
85
109
 
86
110
  def connect
@@ -89,8 +113,36 @@ class Redis
89
113
  # Don't try to reconnect when the connection is fresh
90
114
  with_reconnect(false) do
91
115
  establish_connection
92
- call [:auth, password] if password
116
+ if password
117
+ if username
118
+ begin
119
+ call [:auth, username, password]
120
+ rescue CommandError => err # Likely on Redis < 6
121
+ case err.message
122
+ when /ERR wrong number of arguments for 'auth' command/
123
+ call [:auth, password]
124
+ when /WRONGPASS invalid username-password pair/
125
+ begin
126
+ call [:auth, password]
127
+ rescue CommandError
128
+ raise err
129
+ end
130
+ ::Redis.deprecate!(
131
+ "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
132
+ " the provided password was for the default user. This will start failing in redis-rb 5.0.0."
133
+ )
134
+ else
135
+ raise
136
+ end
137
+ end
138
+ else
139
+ call [:auth, password]
140
+ end
141
+ end
142
+
143
+ call [:readonly] if @options[:readonly]
93
144
  call [:select, db] if db != 0
145
+ call [:client, :setname, @options[:id]] if @options[:id]
94
146
  @connector.check(self)
95
147
  end
96
148
 
@@ -105,21 +157,21 @@ class Redis
105
157
  path || "#{host}:#{port}"
106
158
  end
107
159
 
108
- def call(command, &block)
160
+ def call(command)
109
161
  reply = process([command]) { read }
110
162
  raise reply if reply.is_a?(CommandError)
111
163
 
112
- if block
113
- block.call(reply)
164
+ if block_given? && reply != 'QUEUED'
165
+ yield reply
114
166
  else
115
167
  reply
116
168
  end
117
169
  end
118
170
 
119
- def call_loop(command)
171
+ def call_loop(command, timeout = 0)
120
172
  error = nil
121
173
 
122
- result = without_socket_timeout do
174
+ result = with_socket_timeout(timeout) do
123
175
  process([command]) do
124
176
  loop do
125
177
  reply = read
@@ -141,13 +193,16 @@ class Redis
141
193
  end
142
194
 
143
195
  def call_pipeline(pipeline)
196
+ return [] if pipeline.futures.empty?
197
+
144
198
  with_reconnect pipeline.with_reconnect? do
145
199
  begin
146
- pipeline.finish(call_pipelined(pipeline.commands)).tap do
200
+ pipeline.finish(call_pipelined(pipeline)).tap do
147
201
  self.db = pipeline.db if pipeline.db
148
202
  end
149
203
  rescue ConnectionError => e
150
204
  return nil if pipeline.shutdown?
205
+
151
206
  # Assume the pipeline was sent in one piece, but execution of
152
207
  # SHUTDOWN caused none of the replies for commands that were executed
153
208
  # prior to it from coming back around.
@@ -156,8 +211,8 @@ class Redis
156
211
  end
157
212
  end
158
213
 
159
- def call_pipelined(commands)
160
- return [] if commands.empty?
214
+ def call_pipelined(pipeline)
215
+ return [] if pipeline.futures.empty?
161
216
 
162
217
  # The method #ensure_connected (called from #process) reconnects once on
163
218
  # I/O errors. To make an effort in making sure that commands are not
@@ -167,19 +222,28 @@ class Redis
167
222
  # already successfully executed commands. To circumvent this, don't retry
168
223
  # after the first reply has been read successfully.
169
224
 
225
+ commands = pipeline.commands
226
+
170
227
  result = Array.new(commands.size)
171
228
  reconnect = @reconnect
172
229
 
173
230
  begin
174
- process(commands) do
175
- result[0] = read
176
-
177
- @reconnect = false
231
+ exception = nil
178
232
 
179
- (commands.size - 1).times do |i|
180
- result[i + 1] = read
233
+ process(commands) do
234
+ pipeline.timeouts.each_with_index do |timeout, i|
235
+ reply = if timeout
236
+ with_socket_timeout(timeout) { read }
237
+ else
238
+ read
239
+ end
240
+ result[i] = reply
241
+ @reconnect = false
242
+ exception = reply if exception.nil? && reply.is_a?(CommandError)
181
243
  end
182
244
  end
245
+
246
+ raise exception if exception
183
247
  ensure
184
248
  @reconnect = reconnect
185
249
  end
@@ -187,7 +251,8 @@ class Redis
187
251
  result
188
252
  end
189
253
 
190
- def call_with_timeout(command, timeout, &blk)
254
+ def call_with_timeout(command, extra_timeout, &blk)
255
+ timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
191
256
  with_socket_timeout(timeout) do
192
257
  call(command, &blk)
193
258
  end
@@ -217,12 +282,13 @@ class Redis
217
282
  end
218
283
 
219
284
  def connected?
220
- !! (connection && connection.connected?)
285
+ !!(connection && connection.connected?)
221
286
  end
222
287
 
223
288
  def disconnect
224
289
  connection.disconnect if connected?
225
290
  end
291
+ alias close disconnect
226
292
 
227
293
  def reconnect
228
294
  disconnect
@@ -242,24 +308,30 @@ class Redis
242
308
 
243
309
  def read
244
310
  io do
245
- connection.read
311
+ value = connection.read
312
+ @pending_reads -= 1
313
+ value
246
314
  end
247
315
  end
248
316
 
249
317
  def write(command)
250
318
  io do
319
+ @pending_reads += 1
251
320
  connection.write(command)
252
321
  end
253
322
  end
254
323
 
255
324
  def with_socket_timeout(timeout)
256
325
  connect unless connected?
326
+ original = @options[:read_timeout]
257
327
 
258
328
  begin
259
329
  connection.timeout = timeout
330
+ @options[:read_timeout] = timeout # for reconnection
260
331
  yield
261
332
  ensure
262
333
  connection.timeout = self.timeout if connected?
334
+ @options[:read_timeout] = original
263
335
  end
264
336
  end
265
337
 
@@ -267,30 +339,27 @@ class Redis
267
339
  with_socket_timeout(0, &blk)
268
340
  end
269
341
 
270
- def with_reconnect(val=true)
271
- begin
272
- original, @reconnect = @reconnect, val
273
- yield
274
- ensure
275
- @reconnect = original
276
- end
342
+ def with_reconnect(val = true)
343
+ original, @reconnect = @reconnect, val
344
+ yield
345
+ ensure
346
+ @reconnect = original
277
347
  end
278
348
 
279
349
  def without_reconnect(&blk)
280
350
  with_reconnect(false, &blk)
281
351
  end
282
352
 
283
- protected
353
+ protected
284
354
 
285
355
  def logging(commands)
286
- return yield unless @logger && @logger.debug?
356
+ return yield unless @logger&.debug?
287
357
 
288
358
  begin
289
359
  commands.each do |name, *args|
290
360
  logged_args = args.map do |a|
291
- case
292
- when a.respond_to?(:inspect) then a.inspect
293
- when a.respond_to?(:to_s) then a.to_s
361
+ if a.respond_to?(:inspect) then a.inspect
362
+ elsif a.respond_to?(:to_s) then a.to_s
294
363
  else
295
364
  # handle poorly-behaved descendants of BasicObject
296
365
  klass = a.instance_exec { (class << self; self end).superclass }
@@ -311,16 +380,27 @@ class Redis
311
380
  server = @connector.resolve.dup
312
381
 
313
382
  @options[:host] = server[:host]
314
- @options[:port] = server[:port]
383
+ @options[:port] = Integer(server[:port]) if server.include?(:port)
384
+
385
+ @connection = @options[:driver].connect(@options)
386
+ @pending_reads = 0
387
+ rescue TimeoutError,
388
+ SocketError,
389
+ Errno::EADDRNOTAVAIL,
390
+ Errno::ECONNREFUSED,
391
+ Errno::EHOSTDOWN,
392
+ Errno::EHOSTUNREACH,
393
+ Errno::ENETUNREACH,
394
+ Errno::ENOENT,
395
+ Errno::ETIMEDOUT,
396
+ Errno::EINVAL => error
315
397
 
316
- @connection = @options[:driver].connect(server)
317
- rescue TimeoutError
318
- raise CannotConnectError, "Timed out connecting to Redis on #{location}"
319
- rescue Errno::ECONNREFUSED
320
- raise CannotConnectError, "Error connecting to Redis on #{location} (ECONNREFUSED)"
398
+ raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
321
399
  end
322
400
 
323
401
  def ensure_connected
402
+ disconnect if @pending_reads > 0
403
+
324
404
  attempts = 0
325
405
 
326
406
  begin
@@ -329,19 +409,23 @@ class Redis
329
409
  if connected?
330
410
  unless inherit_socket? || Process.pid == @pid
331
411
  raise InheritedError,
332
- "Tried to use a connection from a child process without reconnecting. " +
333
- "You need to reconnect to Redis after forking " +
334
- "or set :inherit_socket to true."
412
+ "Tried to use a connection from a child process without reconnecting. " \
413
+ "You need to reconnect to Redis after forking " \
414
+ "or set :inherit_socket to true."
335
415
  end
336
416
  else
337
417
  connect
338
418
  end
339
419
 
340
420
  yield
341
- rescue ConnectionError, InheritedError
421
+ rescue BaseConnectionError
342
422
  disconnect
343
423
 
344
424
  if attempts <= @options[:reconnect_attempts] && @reconnect
425
+ sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
426
+ @options[:reconnect_delay_max]].min
427
+
428
+ Kernel.sleep(sleep_t)
345
429
  retry
346
430
  else
347
431
  raise
@@ -353,20 +437,21 @@ class Redis
353
437
  end
354
438
 
355
439
  def _parse_options(options)
440
+ return options if options[:_parsed]
441
+
356
442
  defaults = DEFAULTS.dup
357
443
  options = options.dup
358
444
 
359
- defaults.keys.each do |key|
445
+ defaults.each_key do |key|
360
446
  # Fill in defaults if needed
361
- if defaults[key].respond_to?(:call)
362
- defaults[key] = defaults[key].call
363
- end
447
+ defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
364
448
 
365
449
  # Symbolize only keys that are needed
366
- options[key] = options[key.to_s] if options.has_key?(key.to_s)
450
+ options[key] = options[key.to_s] if options.key?(key.to_s)
367
451
  end
368
452
 
369
- url = options[:url] || defaults[:url]
453
+ url = options[:url]
454
+ url = defaults[:url] if url.nil?
370
455
 
371
456
  # Override defaults from URL if given
372
457
  if url
@@ -374,25 +459,26 @@ class Redis
374
459
 
375
460
  uri = URI(url)
376
461
 
377
- if uri.scheme == "unix"
378
- defaults[:path] = uri.path
379
- elsif uri.scheme == "redis"
380
- # Require the URL to have at least a host
381
- raise ArgumentError, "invalid url" unless uri.host
382
-
462
+ case uri.scheme
463
+ when "unix"
464
+ defaults[:path] = uri.path
465
+ when "redis", "rediss"
383
466
  defaults[:scheme] = uri.scheme
384
- defaults[:host] = uri.host
467
+ defaults[:host] = uri.host if uri.host
385
468
  defaults[:port] = uri.port if uri.port
386
- defaults[:password] = CGI.unescape(uri.password) if uri.password
469
+ defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
470
+ defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
387
471
  defaults[:db] = uri.path[1..-1].to_i if uri.path
388
472
  defaults[:role] = :master
389
473
  else
390
474
  raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
391
475
  end
476
+
477
+ defaults[:ssl] = true if uri.scheme == "rediss"
392
478
  end
393
479
 
394
480
  # Use default when option is not specified or nil
395
- defaults.keys.each do |key|
481
+ defaults.each_key do |key|
396
482
  options[key] = defaults[key] if options[key].nil?
397
483
  end
398
484
 
@@ -407,43 +493,63 @@ class Redis
407
493
  options[:port] = options[:port].to_i
408
494
  end
409
495
 
410
- options[:timeout] = options[:timeout].to_f
496
+ if options.key?(:timeout)
497
+ options[:connect_timeout] ||= options[:timeout]
498
+ options[:read_timeout] ||= options[:timeout]
499
+ options[:write_timeout] ||= options[:timeout]
500
+ end
501
+
502
+ options[:connect_timeout] = Float(options[:connect_timeout])
503
+ options[:read_timeout] = Float(options[:read_timeout])
504
+ options[:write_timeout] = Float(options[:write_timeout])
505
+
506
+ options[:reconnect_attempts] = options[:reconnect_attempts].to_i
507
+ options[:reconnect_delay] = options[:reconnect_delay].to_f
508
+ options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
509
+
411
510
  options[:db] = options[:db].to_i
412
511
  options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
413
512
 
414
513
  case options[:tcp_keepalive]
415
514
  when Hash
416
- [:time, :intvl, :probes].each do |key|
417
- unless options[:tcp_keepalive][key].is_a?(Fixnum)
418
- raise "Expected the #{key.inspect} key in :tcp_keepalive to be a Fixnum"
515
+ %i[time intvl probes].each do |key|
516
+ unless options[:tcp_keepalive][key].is_a?(Integer)
517
+ raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
419
518
  end
420
519
  end
421
520
 
422
- when Fixnum
521
+ when Integer
423
522
  if options[:tcp_keepalive] >= 60
424
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 20, :intvl => 10, :probes => 2}
523
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
425
524
 
426
525
  elsif options[:tcp_keepalive] >= 30
427
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 10, :intvl => 5, :probes => 2}
526
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
428
527
 
429
528
  elsif options[:tcp_keepalive] >= 5
430
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 2, :intvl => 2, :probes => 1}
529
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
431
530
  end
432
531
  end
433
532
 
533
+ options[:_parsed] = true
534
+
434
535
  options
435
536
  end
436
537
 
437
538
  def _parse_driver(driver)
438
539
  driver = driver.to_s if driver.is_a?(Symbol)
439
540
 
440
- if driver.kind_of?(String)
541
+ if driver.is_a?(String)
441
542
  begin
442
- require "redis/connection/#{driver}"
443
- driver = Connection.const_get(driver.capitalize)
543
+ require_relative "connection/#{driver}"
444
544
  rescue LoadError, NameError
445
- raise RuntimeError, "Cannot load driver #{driver.inspect}"
545
+ begin
546
+ require "redis/connection/#{driver}"
547
+ rescue LoadError, NameError => error
548
+ raise "Cannot load driver #{driver.inspect}: #{error.message}"
549
+ end
446
550
  end
551
+
552
+ driver = Connection.const_get(driver.capitalize)
447
553
  end
448
554
 
449
555
  driver
@@ -451,23 +557,24 @@ class Redis
451
557
 
452
558
  class Connector
453
559
  def initialize(options)
454
- @options = options
560
+ @options = options.dup
455
561
  end
456
562
 
457
563
  def resolve
458
564
  @options
459
565
  end
460
566
 
461
- def check(client)
462
- end
567
+ def check(client); end
463
568
 
464
569
  class Sentinel < Connector
465
570
  def initialize(options)
466
571
  super(options)
467
572
 
468
- @sentinels = options.fetch(:sentinels).dup
469
- @role = options[:role].to_s
470
- @master = options[:host]
573
+ @options[:db] = DEFAULTS.fetch(:db)
574
+
575
+ @sentinels = @options.delete(:sentinels).dup
576
+ @role = (@options[:role] || "master").to_s
577
+ @master = @options[:host]
471
578
  end
472
579
 
473
580
  def check(client)
@@ -482,27 +589,33 @@ class Redis
482
589
  end
483
590
 
484
591
  if role != @role
485
- disconnect
592
+ client.disconnect
486
593
  raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
487
594
  end
488
595
  end
489
596
 
490
597
  def resolve
491
598
  result = case @role
492
- when "master"
493
- resolve_master
494
- when "slave"
495
- resolve_slave
496
- else
497
- raise ArgumentError, "Unknown instance role #{@role}"
498
- end
599
+ when "master"
600
+ resolve_master
601
+ when "slave"
602
+ resolve_slave
603
+ else
604
+ raise ArgumentError, "Unknown instance role #{@role}"
605
+ end
499
606
 
500
607
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
501
608
  end
502
609
 
503
610
  def sentinel_detect
504
611
  @sentinels.each do |sentinel|
505
- client = Client.new(:host => sentinel[:host], :port => sentinel[:port], :timeout => 0.3)
612
+ client = Client.new(@options.merge({
613
+ host: sentinel[:host] || sentinel["host"],
614
+ port: sentinel[:port] || sentinel["port"],
615
+ username: sentinel[:username] || sentinel["username"],
616
+ password: sentinel[:password] || sentinel["password"],
617
+ reconnect_attempts: 0
618
+ }))
506
619
 
507
620
  begin
508
621
  if result = yield(client)
@@ -512,18 +625,19 @@ class Redis
512
625
 
513
626
  return result
514
627
  end
628
+ rescue BaseConnectionError
515
629
  ensure
516
630
  client.disconnect
517
631
  end
518
632
  end
519
633
 
520
- return nil
634
+ raise CannotConnectError, "No sentinels available."
521
635
  end
522
636
 
523
637
  def resolve_master
524
638
  sentinel_detect do |client|
525
639
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
526
- {:host => reply[0], :port => reply[1]}
640
+ { host: reply[0], port: reply[1] }
527
641
  end
528
642
  end
529
643
  end
@@ -531,9 +645,19 @@ class Redis
531
645
  def resolve_slave
532
646
  sentinel_detect do |client|
533
647
  if reply = client.call(["sentinel", "slaves", @master])
534
- slave = Hash[*reply.sample]
535
-
536
- {:host => slave.fetch("ip"), :port => slave.fetch("port")}
648
+ slaves = reply.map { |s| s.each_slice(2).to_h }
649
+ slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
650
+ slaves.reject! { |s| s.fetch('flags').include?('s_down') }
651
+
652
+ if slaves.empty?
653
+ raise CannotConnectError, 'No slaves available.'
654
+ else
655
+ slave = slaves.sample
656
+ {
657
+ host: slave.fetch('ip'),
658
+ port: slave.fetch('port')
659
+ }
660
+ end
537
661
  end
538
662
  end
539
663
  end