redis 4.0.3 → 4.5.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 (157) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +110 -0
  3. data/README.md +126 -17
  4. data/lib/redis/client.rb +130 -82
  5. data/lib/redis/cluster/command_loader.rb +8 -7
  6. data/lib/redis/cluster/node.rb +5 -1
  7. data/lib/redis/cluster/node_key.rb +3 -7
  8. data/lib/redis/cluster/node_loader.rb +2 -0
  9. data/lib/redis/cluster/option.rb +31 -14
  10. data/lib/redis/cluster/slot.rb +30 -13
  11. data/lib/redis/cluster/slot_loader.rb +6 -4
  12. data/lib/redis/cluster.rb +23 -17
  13. data/lib/redis/connection/command_helper.rb +5 -2
  14. data/lib/redis/connection/hiredis.rb +4 -3
  15. data/lib/redis/connection/registry.rb +2 -1
  16. data/lib/redis/connection/ruby.rb +139 -106
  17. data/lib/redis/connection/synchrony.rb +9 -4
  18. data/lib/redis/connection.rb +2 -0
  19. data/lib/redis/distributed.rb +171 -70
  20. data/lib/redis/errors.rb +2 -0
  21. data/lib/redis/hash_ring.rb +15 -14
  22. data/lib/redis/pipeline.rb +46 -8
  23. data/lib/redis/subscribe.rb +11 -12
  24. data/lib/redis/version.rb +3 -1
  25. data/lib/redis.rb +1239 -426
  26. metadata +16 -262
  27. data/.gitignore +0 -19
  28. data/.travis/Gemfile +0 -18
  29. data/.travis.yml +0 -61
  30. data/.yardopts +0 -3
  31. data/Gemfile +0 -8
  32. data/benchmarking/logging.rb +0 -71
  33. data/benchmarking/pipeline.rb +0 -51
  34. data/benchmarking/speed.rb +0 -21
  35. data/benchmarking/suite.rb +0 -24
  36. data/benchmarking/worker.rb +0 -71
  37. data/bin/build +0 -71
  38. data/bors.toml +0 -14
  39. data/examples/basic.rb +0 -15
  40. data/examples/consistency.rb +0 -114
  41. data/examples/dist_redis.rb +0 -43
  42. data/examples/incr-decr.rb +0 -17
  43. data/examples/list.rb +0 -26
  44. data/examples/pubsub.rb +0 -37
  45. data/examples/sentinel/sentinel.conf +0 -9
  46. data/examples/sentinel/start +0 -49
  47. data/examples/sentinel.rb +0 -41
  48. data/examples/sets.rb +0 -36
  49. data/examples/unicorn/config.ru +0 -3
  50. data/examples/unicorn/unicorn.rb +0 -20
  51. data/makefile +0 -74
  52. data/redis.gemspec +0 -43
  53. data/test/bitpos_test.rb +0 -63
  54. data/test/blocking_commands_test.rb +0 -40
  55. data/test/client_test.rb +0 -76
  56. data/test/cluster_abnormal_state_test.rb +0 -38
  57. data/test/cluster_blocking_commands_test.rb +0 -15
  58. data/test/cluster_client_internals_test.rb +0 -77
  59. data/test/cluster_client_key_hash_tags_test.rb +0 -88
  60. data/test/cluster_client_options_test.rb +0 -147
  61. data/test/cluster_client_pipelining_test.rb +0 -59
  62. data/test/cluster_client_replicas_test.rb +0 -36
  63. data/test/cluster_client_slots_test.rb +0 -94
  64. data/test/cluster_client_transactions_test.rb +0 -71
  65. data/test/cluster_commands_on_cluster_test.rb +0 -165
  66. data/test/cluster_commands_on_connection_test.rb +0 -40
  67. data/test/cluster_commands_on_geo_test.rb +0 -74
  68. data/test/cluster_commands_on_hashes_test.rb +0 -11
  69. data/test/cluster_commands_on_hyper_log_log_test.rb +0 -17
  70. data/test/cluster_commands_on_keys_test.rb +0 -134
  71. data/test/cluster_commands_on_lists_test.rb +0 -15
  72. data/test/cluster_commands_on_pub_sub_test.rb +0 -101
  73. data/test/cluster_commands_on_scripting_test.rb +0 -56
  74. data/test/cluster_commands_on_server_test.rb +0 -221
  75. data/test/cluster_commands_on_sets_test.rb +0 -39
  76. data/test/cluster_commands_on_sorted_sets_test.rb +0 -35
  77. data/test/cluster_commands_on_streams_test.rb +0 -196
  78. data/test/cluster_commands_on_strings_test.rb +0 -15
  79. data/test/cluster_commands_on_transactions_test.rb +0 -41
  80. data/test/cluster_commands_on_value_types_test.rb +0 -14
  81. data/test/command_map_test.rb +0 -28
  82. data/test/commands_on_geo_test.rb +0 -116
  83. data/test/commands_on_hashes_test.rb +0 -7
  84. data/test/commands_on_hyper_log_log_test.rb +0 -7
  85. data/test/commands_on_lists_test.rb +0 -7
  86. data/test/commands_on_sets_test.rb +0 -7
  87. data/test/commands_on_sorted_sets_test.rb +0 -7
  88. data/test/commands_on_strings_test.rb +0 -7
  89. data/test/commands_on_value_types_test.rb +0 -207
  90. data/test/connection_handling_test.rb +0 -275
  91. data/test/connection_test.rb +0 -57
  92. data/test/db/.gitkeep +0 -0
  93. data/test/distributed_blocking_commands_test.rb +0 -52
  94. data/test/distributed_commands_on_hashes_test.rb +0 -21
  95. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -26
  96. data/test/distributed_commands_on_lists_test.rb +0 -19
  97. data/test/distributed_commands_on_sets_test.rb +0 -105
  98. data/test/distributed_commands_on_sorted_sets_test.rb +0 -59
  99. data/test/distributed_commands_on_strings_test.rb +0 -79
  100. data/test/distributed_commands_on_value_types_test.rb +0 -129
  101. data/test/distributed_commands_requiring_clustering_test.rb +0 -162
  102. data/test/distributed_connection_handling_test.rb +0 -21
  103. data/test/distributed_internals_test.rb +0 -68
  104. data/test/distributed_key_tags_test.rb +0 -50
  105. data/test/distributed_persistence_control_commands_test.rb +0 -24
  106. data/test/distributed_publish_subscribe_test.rb +0 -90
  107. data/test/distributed_remote_server_control_commands_test.rb +0 -64
  108. data/test/distributed_scripting_test.rb +0 -100
  109. data/test/distributed_sorting_test.rb +0 -18
  110. data/test/distributed_test.rb +0 -56
  111. data/test/distributed_transactions_test.rb +0 -30
  112. data/test/encoding_test.rb +0 -14
  113. data/test/error_replies_test.rb +0 -57
  114. data/test/fork_safety_test.rb +0 -60
  115. data/test/helper.rb +0 -345
  116. data/test/helper_test.rb +0 -22
  117. data/test/internals_test.rb +0 -408
  118. data/test/lint/blocking_commands.rb +0 -174
  119. data/test/lint/hashes.rb +0 -203
  120. data/test/lint/hyper_log_log.rb +0 -74
  121. data/test/lint/lists.rb +0 -159
  122. data/test/lint/sets.rb +0 -282
  123. data/test/lint/sorted_sets.rb +0 -497
  124. data/test/lint/strings.rb +0 -348
  125. data/test/lint/value_types.rb +0 -130
  126. data/test/persistence_control_commands_test.rb +0 -24
  127. data/test/pipelining_commands_test.rb +0 -246
  128. data/test/publish_subscribe_test.rb +0 -280
  129. data/test/remote_server_control_commands_test.rb +0 -175
  130. data/test/scanning_test.rb +0 -407
  131. data/test/scripting_test.rb +0 -76
  132. data/test/sentinel_command_test.rb +0 -78
  133. data/test/sentinel_test.rb +0 -253
  134. data/test/sorting_test.rb +0 -57
  135. data/test/ssl_test.rb +0 -69
  136. data/test/support/cluster/orchestrator.rb +0 -199
  137. data/test/support/connection/hiredis.rb +0 -1
  138. data/test/support/connection/ruby.rb +0 -1
  139. data/test/support/connection/synchrony.rb +0 -17
  140. data/test/support/redis_mock.rb +0 -130
  141. data/test/support/ssl/gen_certs.sh +0 -31
  142. data/test/support/ssl/trusted-ca.crt +0 -25
  143. data/test/support/ssl/trusted-ca.key +0 -27
  144. data/test/support/ssl/trusted-cert.crt +0 -81
  145. data/test/support/ssl/trusted-cert.key +0 -28
  146. data/test/support/ssl/untrusted-ca.crt +0 -26
  147. data/test/support/ssl/untrusted-ca.key +0 -27
  148. data/test/support/ssl/untrusted-cert.crt +0 -82
  149. data/test/support/ssl/untrusted-cert.key +0 -28
  150. data/test/support/wire/synchrony.rb +0 -24
  151. data/test/support/wire/thread.rb +0 -5
  152. data/test/synchrony_driver.rb +0 -85
  153. data/test/test.conf.erb +0 -9
  154. data/test/thread_safety_test.rb +0 -60
  155. data/test/transactions_test.rb +0 -272
  156. data/test/unknown_commands_test.rb +0 -12
  157. data/test/url_param_test.rb +0 -136
data/lib/redis/client.rb CHANGED
@@ -1,27 +1,36 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "errors"
2
4
  require "socket"
3
5
  require "cgi"
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
- :reconnect_delay => 0,
22
- :reconnect_delay_max => 0.5,
23
- :inherit_socket => false
24
- }
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
25
34
 
26
35
  attr_reader :options
27
36
 
@@ -53,6 +62,10 @@ class Redis
53
62
  @options[:read_timeout]
54
63
  end
55
64
 
65
+ def username
66
+ @options[:username]
67
+ end
68
+
56
69
  def password
57
70
  @options[:password]
58
71
  end
@@ -87,7 +100,7 @@ class Redis
87
100
  @pending_reads = 0
88
101
 
89
102
  @connector =
90
- if options.include?(:sentinels)
103
+ if !@options[:sentinels].nil?
91
104
  Connector::Sentinel.new(@options)
92
105
  elsif options.include?(:connector) && options[:connector].respond_to?(:new)
93
106
  options.delete(:connector).new(@options)
@@ -102,7 +115,23 @@ class Redis
102
115
  # Don't try to reconnect when the connection is fresh
103
116
  with_reconnect(false) do
104
117
  establish_connection
105
- call [:auth, password] if password
118
+ if password
119
+ if username
120
+ begin
121
+ call [:auth, username, password]
122
+ rescue CommandError => err # Likely on Redis < 6
123
+ if err.message.match?(/ERR wrong number of arguments for \'auth\' command/)
124
+ call [:auth, password]
125
+ else
126
+ raise
127
+ end
128
+ end
129
+ else
130
+ call [:auth, password]
131
+ end
132
+ end
133
+
134
+ call [:readonly] if @options[:readonly]
106
135
  call [:select, db] if db != 0
107
136
  call [:client, :setname, @options[:id]] if @options[:id]
108
137
  @connector.check(self)
@@ -123,7 +152,7 @@ class Redis
123
152
  reply = process([command]) { read }
124
153
  raise reply if reply.is_a?(CommandError)
125
154
 
126
- if block_given?
155
+ if block_given? && reply != 'QUEUED'
127
156
  yield reply
128
157
  else
129
158
  reply
@@ -155,16 +184,16 @@ class Redis
155
184
  end
156
185
 
157
186
  def call_pipeline(pipeline)
158
- commands = pipeline.commands
159
- return [] if commands.empty?
187
+ return [] if pipeline.futures.empty?
160
188
 
161
189
  with_reconnect pipeline.with_reconnect? do
162
190
  begin
163
- pipeline.finish(call_pipelined(commands)).tap do
191
+ pipeline.finish(call_pipelined(pipeline)).tap do
164
192
  self.db = pipeline.db if pipeline.db
165
193
  end
166
194
  rescue ConnectionError => e
167
195
  return nil if pipeline.shutdown?
196
+
168
197
  # Assume the pipeline was sent in one piece, but execution of
169
198
  # SHUTDOWN caused none of the replies for commands that were executed
170
199
  # prior to it from coming back around.
@@ -173,8 +202,8 @@ class Redis
173
202
  end
174
203
  end
175
204
 
176
- def call_pipelined(commands)
177
- return [] if commands.empty?
205
+ def call_pipelined(pipeline)
206
+ return [] if pipeline.futures.empty?
178
207
 
179
208
  # The method #ensure_connected (called from #process) reconnects once on
180
209
  # I/O errors. To make an effort in making sure that commands are not
@@ -184,6 +213,8 @@ class Redis
184
213
  # already successfully executed commands. To circumvent this, don't retry
185
214
  # after the first reply has been read successfully.
186
215
 
216
+ commands = pipeline.commands
217
+
187
218
  result = Array.new(commands.size)
188
219
  reconnect = @reconnect
189
220
 
@@ -191,8 +222,12 @@ class Redis
191
222
  exception = nil
192
223
 
193
224
  process(commands) do
194
- commands.size.times do |i|
195
- reply = read
225
+ pipeline.timeouts.each_with_index do |timeout, i|
226
+ reply = if timeout
227
+ with_socket_timeout(timeout) { read }
228
+ else
229
+ read
230
+ end
196
231
  result[i] = reply
197
232
  @reconnect = false
198
233
  exception = reply if exception.nil? && reply.is_a?(CommandError)
@@ -237,12 +272,13 @@ class Redis
237
272
  end
238
273
 
239
274
  def connected?
240
- !! (connection && connection.connected?)
275
+ !!(connection && connection.connected?)
241
276
  end
242
277
 
243
278
  def disconnect
244
279
  connection.disconnect if connected?
245
280
  end
281
+ alias close disconnect
246
282
 
247
283
  def reconnect
248
284
  disconnect
@@ -277,12 +313,15 @@ class Redis
277
313
 
278
314
  def with_socket_timeout(timeout)
279
315
  connect unless connected?
316
+ original = @options[:read_timeout]
280
317
 
281
318
  begin
282
319
  connection.timeout = timeout
320
+ @options[:read_timeout] = timeout # for reconnection
283
321
  yield
284
322
  ensure
285
323
  connection.timeout = self.timeout if connected?
324
+ @options[:read_timeout] = original
286
325
  end
287
326
  end
288
327
 
@@ -290,30 +329,27 @@ class Redis
290
329
  with_socket_timeout(0, &blk)
291
330
  end
292
331
 
293
- def with_reconnect(val=true)
294
- begin
295
- original, @reconnect = @reconnect, val
296
- yield
297
- ensure
298
- @reconnect = original
299
- end
332
+ def with_reconnect(val = true)
333
+ original, @reconnect = @reconnect, val
334
+ yield
335
+ ensure
336
+ @reconnect = original
300
337
  end
301
338
 
302
339
  def without_reconnect(&blk)
303
340
  with_reconnect(false, &blk)
304
341
  end
305
342
 
306
- protected
343
+ protected
307
344
 
308
345
  def logging(commands)
309
- return yield unless @logger && @logger.debug?
346
+ return yield unless @logger&.debug?
310
347
 
311
348
  begin
312
349
  commands.each do |name, *args|
313
350
  logged_args = args.map do |a|
314
- case
315
- when a.respond_to?(:inspect) then a.inspect
316
- when a.respond_to?(:to_s) then a.to_s
351
+ if a.respond_to?(:inspect) then a.inspect
352
+ elsif a.respond_to?(:to_s) then a.to_s
317
353
  else
318
354
  # handle poorly-behaved descendants of BasicObject
319
355
  klass = a.instance_exec { (class << self; self end).superclass }
@@ -340,14 +376,16 @@ class Redis
340
376
  @pending_reads = 0
341
377
  rescue TimeoutError,
342
378
  SocketError,
379
+ Errno::EADDRNOTAVAIL,
343
380
  Errno::ECONNREFUSED,
344
381
  Errno::EHOSTDOWN,
345
382
  Errno::EHOSTUNREACH,
346
383
  Errno::ENETUNREACH,
347
384
  Errno::ENOENT,
348
- Errno::ETIMEDOUT
385
+ Errno::ETIMEDOUT,
386
+ Errno::EINVAL => error
349
387
 
350
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
388
+ raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
351
389
  end
352
390
 
353
391
  def ensure_connected
@@ -361,9 +399,9 @@ class Redis
361
399
  if connected?
362
400
  unless inherit_socket? || Process.pid == @pid
363
401
  raise InheritedError,
364
- "Tried to use a connection from a child process without reconnecting. " +
365
- "You need to reconnect to Redis after forking " +
366
- "or set :inherit_socket to true."
402
+ "Tried to use a connection from a child process without reconnecting. " \
403
+ "You need to reconnect to Redis after forking " \
404
+ "or set :inherit_socket to true."
367
405
  end
368
406
  else
369
407
  connect
@@ -374,7 +412,7 @@ class Redis
374
412
  disconnect
375
413
 
376
414
  if attempts <= @options[:reconnect_attempts] && @reconnect
377
- sleep_t = [(@options[:reconnect_delay] * 2**(attempts-1)),
415
+ sleep_t = [(@options[:reconnect_delay] * 2**(attempts - 1)),
378
416
  @options[:reconnect_delay_max]].min
379
417
 
380
418
  Kernel.sleep(sleep_t)
@@ -396,15 +434,14 @@ class Redis
396
434
 
397
435
  defaults.keys.each do |key|
398
436
  # Fill in defaults if needed
399
- if defaults[key].respond_to?(:call)
400
- defaults[key] = defaults[key].call
401
- end
437
+ defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
402
438
 
403
439
  # Symbolize only keys that are needed
404
- options[key] = options[key.to_s] if options.has_key?(key.to_s)
440
+ options[key] = options[key.to_s] if options.key?(key.to_s)
405
441
  end
406
442
 
407
- url = options[:url] || defaults[:url]
443
+ url = options[:url]
444
+ url = defaults[:url] if url.nil?
408
445
 
409
446
  # Override defaults from URL if given
410
447
  if url
@@ -413,12 +450,13 @@ class Redis
413
450
  uri = URI(url)
414
451
 
415
452
  if uri.scheme == "unix"
416
- defaults[:path] = uri.path
453
+ defaults[:path] = uri.path
417
454
  elsif uri.scheme == "redis" || uri.scheme == "rediss"
418
455
  defaults[:scheme] = uri.scheme
419
456
  defaults[:host] = uri.host if uri.host
420
457
  defaults[:port] = uri.port if uri.port
421
- defaults[:password] = CGI.unescape(uri.password) if uri.password
458
+ defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
459
+ defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
422
460
  defaults[:db] = uri.path[1..-1].to_i if uri.path
423
461
  defaults[:role] = :master
424
462
  else
@@ -444,7 +482,7 @@ class Redis
444
482
  options[:port] = options[:port].to_i
445
483
  end
446
484
 
447
- if options.has_key?(:timeout)
485
+ if options.key?(:timeout)
448
486
  options[:connect_timeout] ||= options[:timeout]
449
487
  options[:read_timeout] ||= options[:timeout]
450
488
  options[:write_timeout] ||= options[:timeout]
@@ -463,7 +501,7 @@ class Redis
463
501
 
464
502
  case options[:tcp_keepalive]
465
503
  when Hash
466
- [:time, :intvl, :probes].each do |key|
504
+ %i[time intvl probes].each do |key|
467
505
  unless options[:tcp_keepalive][key].is_a?(Integer)
468
506
  raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
469
507
  end
@@ -471,13 +509,13 @@ class Redis
471
509
 
472
510
  when Integer
473
511
  if options[:tcp_keepalive] >= 60
474
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 20, :intvl => 10, :probes => 2}
512
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
475
513
 
476
514
  elsif options[:tcp_keepalive] >= 30
477
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 10, :intvl => 5, :probes => 2}
515
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
478
516
 
479
517
  elsif options[:tcp_keepalive] >= 5
480
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 2, :intvl => 2, :probes => 1}
518
+ options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
481
519
  end
482
520
  end
483
521
 
@@ -489,14 +527,14 @@ class Redis
489
527
  def _parse_driver(driver)
490
528
  driver = driver.to_s if driver.is_a?(Symbol)
491
529
 
492
- if driver.kind_of?(String)
530
+ if driver.is_a?(String)
493
531
  begin
494
532
  require_relative "connection/#{driver}"
495
- rescue LoadError, NameError => e
533
+ rescue LoadError, NameError
496
534
  begin
497
- require "connection/#{driver}"
498
- rescue LoadError, NameError => e
499
- raise RuntimeError, "Cannot load driver #{driver.inspect}: #{e.message}"
535
+ require "redis/connection/#{driver}"
536
+ rescue LoadError, NameError => error
537
+ raise "Cannot load driver #{driver.inspect}: #{error.message}"
500
538
  end
501
539
  end
502
540
 
@@ -515,18 +553,16 @@ class Redis
515
553
  @options
516
554
  end
517
555
 
518
- def check(client)
519
- end
556
+ def check(client); end
520
557
 
521
558
  class Sentinel < Connector
522
559
  def initialize(options)
523
560
  super(options)
524
561
 
525
- @options[:password] = DEFAULTS.fetch(:password)
526
562
  @options[:db] = DEFAULTS.fetch(:db)
527
563
 
528
564
  @sentinels = @options.delete(:sentinels).dup
529
- @role = @options.fetch(:role, "master").to_s
565
+ @role = (@options[:role] || "master").to_s
530
566
  @master = @options[:host]
531
567
  end
532
568
 
@@ -549,13 +585,13 @@ class Redis
549
585
 
550
586
  def resolve
551
587
  result = case @role
552
- when "master"
553
- resolve_master
554
- when "slave"
555
- resolve_slave
556
- else
557
- raise ArgumentError, "Unknown instance role #{@role}"
558
- end
588
+ when "master"
589
+ resolve_master
590
+ when "slave"
591
+ resolve_slave
592
+ else
593
+ raise ArgumentError, "Unknown instance role #{@role}"
594
+ end
559
595
 
560
596
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
561
597
  end
@@ -563,10 +599,12 @@ class Redis
563
599
  def sentinel_detect
564
600
  @sentinels.each do |sentinel|
565
601
  client = Client.new(@options.merge({
566
- :host => sentinel[:host],
567
- :port => sentinel[:port],
568
- :reconnect_attempts => 0,
569
- }))
602
+ host: sentinel[:host] || sentinel["host"],
603
+ port: sentinel[:port] || sentinel["port"],
604
+ username: sentinel[:username] || sentinel["username"],
605
+ password: sentinel[:password] || sentinel["password"],
606
+ reconnect_attempts: 0
607
+ }))
570
608
 
571
609
  begin
572
610
  if result = yield(client)
@@ -588,7 +626,7 @@ class Redis
588
626
  def resolve_master
589
627
  sentinel_detect do |client|
590
628
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
591
- {:host => reply[0], :port => reply[1]}
629
+ { host: reply[0], port: reply[1] }
592
630
  end
593
631
  end
594
632
  end
@@ -596,9 +634,19 @@ class Redis
596
634
  def resolve_slave
597
635
  sentinel_detect do |client|
598
636
  if reply = client.call(["sentinel", "slaves", @master])
599
- slave = Hash[*reply.sample]
600
-
601
- {:host => slave.fetch("ip"), :port => slave.fetch("port")}
637
+ slaves = reply.map { |s| s.each_slice(2).to_h }
638
+ slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
639
+ slaves.reject! { |s| s.fetch('flags').include?('s_down') }
640
+
641
+ if slaves.empty?
642
+ raise CannotConnectError, 'No slaves available.'
643
+ else
644
+ slave = slaves.sample
645
+ {
646
+ host: slave.fetch('ip'),
647
+ port: slave.fetch('port')
648
+ }
649
+ end
602
650
  end
603
651
  end
604
652
  end
@@ -10,23 +10,24 @@ class Redis
10
10
  module_function
11
11
 
12
12
  def load(nodes)
13
- details = {}
14
-
15
13
  nodes.each do |node|
16
- details = fetch_command_details(node)
17
- details.empty? ? next : break
14
+ begin
15
+ return fetch_command_details(node)
16
+ rescue CannotConnectError, ConnectionError, CommandError
17
+ next # can retry on another node
18
+ end
18
19
  end
19
20
 
20
- details
21
+ raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
21
22
  end
22
23
 
23
24
  def fetch_command_details(node)
24
25
  node.call(%i[command]).map do |reply|
25
26
  [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
26
27
  end.to_h
27
- rescue CannotConnectError, ConnectionError, CommandError
28
- {} # can retry on another node
29
28
  end
29
+
30
+ private_class_method :fetch_command_details
30
31
  end
31
32
  end
32
33
  end
@@ -39,6 +39,7 @@ class Redis
39
39
  def call_master(command, &block)
40
40
  try_map do |node_key, client|
41
41
  next if slave?(node_key)
42
+
42
43
  client.call(command, &block)
43
44
  end.values
44
45
  end
@@ -48,6 +49,7 @@ class Redis
48
49
 
49
50
  try_map do |node_key, client|
50
51
  next if master?(node_key)
52
+
51
53
  client.call(command, &block)
52
54
  end.values
53
55
  end
@@ -74,8 +76,9 @@ class Redis
74
76
  clients = options.map do |node_key, option|
75
77
  next if replica_disabled? && slave?(node_key)
76
78
 
79
+ option = option.merge(readonly: true) if slave?(node_key)
80
+
77
81
  client = Client.new(option)
78
- client.call(%i[readonly]) if slave?(node_key)
79
82
  [node_key, client]
80
83
  end
81
84
 
@@ -97,6 +100,7 @@ class Redis
97
100
  end
98
101
 
99
102
  return results if errors.empty?
103
+
100
104
  raise CommandErrorCollection, errors
101
105
  end
102
106
  end
@@ -6,17 +6,13 @@ class Redis
6
6
  # It is different from node id.
7
7
  # Node id is internal identifying code in Redis Cluster.
8
8
  module NodeKey
9
- DEFAULT_SCHEME = 'redis'
10
- SECURE_SCHEME = 'rediss'
11
9
  DELIMITER = ':'
12
10
 
13
11
  module_function
14
12
 
15
- def to_node_urls(node_keys, secure:)
16
- scheme = secure ? SECURE_SCHEME : DEFAULT_SCHEME
17
- node_keys
18
- .map { |k| k.split(DELIMITER) }
19
- .map { |k| URI::Generic.build(scheme: scheme, host: k[0], port: k[1].to_i).to_s }
13
+ def optionize(node_key)
14
+ host, port = split(node_key)
15
+ { host: host, port: port }
20
16
  end
21
17
 
22
18
  def split(node_key)
@@ -30,6 +30,8 @@ class Redis
30
30
  rescue CannotConnectError, ConnectionError, CommandError
31
31
  {} # can retry on another node
32
32
  end
33
+
34
+ private_class_method :fetch_node_info
33
35
  end
34
36
  end
35
37
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require_relative '../errors'
4
4
  require_relative 'node_key'
5
+ require 'uri'
5
6
 
6
7
  class Redis
7
8
  class Cluster
@@ -14,36 +15,36 @@ class Redis
14
15
  def initialize(options)
15
16
  options = options.dup
16
17
  node_addrs = options.delete(:cluster)
17
- @node_uris = build_node_uris(node_addrs)
18
+ @node_opts = build_node_options(node_addrs)
18
19
  @replica = options.delete(:replica) == true
20
+ add_common_node_option_if_needed(options, @node_opts, :scheme)
21
+ add_common_node_option_if_needed(options, @node_opts, :username)
22
+ add_common_node_option_if_needed(options, @node_opts, :password)
19
23
  @options = options
20
24
  end
21
25
 
22
26
  def per_node_key
23
- @node_uris.map { |uri| [NodeKey.build_from_uri(uri), @options.merge(url: uri.to_s)] }
27
+ @node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
24
28
  .to_h
25
29
  end
26
30
 
27
- def secure?
28
- @node_uris.any? { |uri| uri.scheme == SECURE_SCHEME } || @options[:ssl_params] || false
29
- end
30
-
31
31
  def use_replica?
32
32
  @replica
33
33
  end
34
34
 
35
35
  def update_node(addrs)
36
- @node_uris = build_node_uris(addrs)
36
+ @node_opts = build_node_options(addrs)
37
37
  end
38
38
 
39
39
  def add_node(host, port)
40
- @node_uris << parse_node_hash(host: host, port: port)
40
+ @node_opts << { host: host, port: port }
41
41
  end
42
42
 
43
43
  private
44
44
 
45
- def build_node_uris(addrs)
45
+ def build_node_options(addrs)
46
46
  raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
47
+
47
48
  addrs.map { |addr| parse_node_addr(addr) }
48
49
  end
49
50
 
@@ -52,7 +53,7 @@ class Redis
52
53
  when String
53
54
  parse_node_url(addr)
54
55
  when Hash
55
- parse_node_hash(addr)
56
+ parse_node_option(addr)
56
57
  else
57
58
  raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
58
59
  end
@@ -61,15 +62,31 @@ class Redis
61
62
  def parse_node_url(addr)
62
63
  uri = URI(addr)
63
64
  raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
64
- uri
65
+
66
+ db = uri.path.split('/')[1]&.to_i
67
+
68
+ { scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
69
+ .reject { |_, v| v.nil? || v == '' }
65
70
  rescue URI::InvalidURIError => err
66
71
  raise InvalidClientOptionError, err.message
67
72
  end
68
73
 
69
- def parse_node_hash(addr)
74
+ def parse_node_option(addr)
70
75
  addr = addr.map { |k, v| [k.to_sym, v] }.to_h
71
- raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys' if addr.values_at(:host, :port).any?(&:nil?)
72
- URI::Generic.build(scheme: DEFAULT_SCHEME, host: addr[:host], port: addr[:port].to_i)
76
+ if addr.values_at(:host, :port).any?(&:nil?)
77
+ raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
78
+ end
79
+
80
+ addr
81
+ end
82
+
83
+ # Redis cluster node returns only host and port information.
84
+ # So we should complement additional information such as:
85
+ # scheme, username, password and so on.
86
+ def add_common_node_option_if_needed(options, node_opts, key)
87
+ return options if options[key].nil? && node_opts.first[key].nil?
88
+
89
+ options[key] ||= node_opts.first[key]
73
90
  end
74
91
  end
75
92
  end