redis 3.3.5 → 4.7.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 (147) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +220 -2
  3. data/README.md +169 -89
  4. data/lib/redis/client.rb +176 -99
  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 +34 -0
  11. data/lib/redis/cluster/option.rb +100 -0
  12. data/lib/redis/cluster/slot.rb +86 -0
  13. data/lib/redis/cluster/slot_loader.rb +46 -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 +812 -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 +139 -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 +5 -5
  34. data/lib/redis/connection/registry.rb +2 -1
  35. data/lib/redis/connection/ruby.rb +130 -128
  36. data/lib/redis/connection/synchrony.rb +24 -9
  37. data/lib/redis/connection.rb +3 -1
  38. data/lib/redis/distributed.rb +231 -72
  39. data/lib/redis/errors.rb +57 -0
  40. data/lib/redis/hash_ring.rb +30 -73
  41. data/lib/redis/pipeline.rb +178 -13
  42. data/lib/redis/subscribe.rb +11 -12
  43. data/lib/redis/version.rb +3 -1
  44. data/lib/redis.rb +173 -2661
  45. metadata +66 -202
  46. data/.gitignore +0 -16
  47. data/.travis/Gemfile +0 -11
  48. data/.travis.yml +0 -89
  49. data/.yardopts +0 -3
  50. data/Gemfile +0 -4
  51. data/Rakefile +0 -87
  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 -44
  70. data/test/bitpos_test.rb +0 -69
  71. data/test/blocking_commands_test.rb +0 -42
  72. data/test/client_test.rb +0 -59
  73. data/test/command_map_test.rb +0 -30
  74. data/test/commands_on_hashes_test.rb +0 -21
  75. data/test/commands_on_hyper_log_log_test.rb +0 -21
  76. data/test/commands_on_lists_test.rb +0 -20
  77. data/test/commands_on_sets_test.rb +0 -77
  78. data/test/commands_on_sorted_sets_test.rb +0 -137
  79. data/test/commands_on_strings_test.rb +0 -101
  80. data/test/commands_on_value_types_test.rb +0 -133
  81. data/test/connection_handling_test.rb +0 -277
  82. data/test/connection_test.rb +0 -57
  83. data/test/db/.gitkeep +0 -0
  84. data/test/distributed_blocking_commands_test.rb +0 -46
  85. data/test/distributed_commands_on_hashes_test.rb +0 -10
  86. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  87. data/test/distributed_commands_on_lists_test.rb +0 -22
  88. data/test/distributed_commands_on_sets_test.rb +0 -83
  89. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  90. data/test/distributed_commands_on_strings_test.rb +0 -59
  91. data/test/distributed_commands_on_value_types_test.rb +0 -95
  92. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  93. data/test/distributed_connection_handling_test.rb +0 -23
  94. data/test/distributed_internals_test.rb +0 -79
  95. data/test/distributed_key_tags_test.rb +0 -52
  96. data/test/distributed_persistence_control_commands_test.rb +0 -26
  97. data/test/distributed_publish_subscribe_test.rb +0 -92
  98. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  99. data/test/distributed_scripting_test.rb +0 -102
  100. data/test/distributed_sorting_test.rb +0 -20
  101. data/test/distributed_test.rb +0 -58
  102. data/test/distributed_transactions_test.rb +0 -32
  103. data/test/encoding_test.rb +0 -18
  104. data/test/error_replies_test.rb +0 -59
  105. data/test/fork_safety_test.rb +0 -65
  106. data/test/helper.rb +0 -232
  107. data/test/helper_test.rb +0 -24
  108. data/test/internals_test.rb +0 -417
  109. data/test/lint/blocking_commands.rb +0 -150
  110. data/test/lint/hashes.rb +0 -162
  111. data/test/lint/hyper_log_log.rb +0 -60
  112. data/test/lint/lists.rb +0 -143
  113. data/test/lint/sets.rb +0 -140
  114. data/test/lint/sorted_sets.rb +0 -316
  115. data/test/lint/strings.rb +0 -260
  116. data/test/lint/value_types.rb +0 -122
  117. data/test/persistence_control_commands_test.rb +0 -26
  118. data/test/pipelining_commands_test.rb +0 -242
  119. data/test/publish_subscribe_test.rb +0 -282
  120. data/test/remote_server_control_commands_test.rb +0 -118
  121. data/test/scanning_test.rb +0 -413
  122. data/test/scripting_test.rb +0 -78
  123. data/test/sentinel_command_test.rb +0 -80
  124. data/test/sentinel_test.rb +0 -255
  125. data/test/sorting_test.rb +0 -59
  126. data/test/ssl_test.rb +0 -73
  127. data/test/support/connection/hiredis.rb +0 -1
  128. data/test/support/connection/ruby.rb +0 -1
  129. data/test/support/connection/synchrony.rb +0 -17
  130. data/test/support/redis_mock.rb +0 -130
  131. data/test/support/ssl/gen_certs.sh +0 -31
  132. data/test/support/ssl/trusted-ca.crt +0 -25
  133. data/test/support/ssl/trusted-ca.key +0 -27
  134. data/test/support/ssl/trusted-cert.crt +0 -81
  135. data/test/support/ssl/trusted-cert.key +0 -28
  136. data/test/support/ssl/untrusted-ca.crt +0 -26
  137. data/test/support/ssl/untrusted-ca.key +0 -27
  138. data/test/support/ssl/untrusted-cert.crt +0 -82
  139. data/test/support/ssl/untrusted-cert.key +0 -28
  140. data/test/support/wire/synchrony.rb +0 -24
  141. data/test/support/wire/thread.rb +0 -5
  142. data/test/synchrony_driver.rb +0 -88
  143. data/test/test.conf.erb +0 -9
  144. data/test/thread_safety_test.rb +0 -62
  145. data/test/transactions_test.rb +0 -264
  146. data/test/unknown_commands_test.rb +0 -14
  147. data/test/url_param_test.rb +0 -138
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]
@@ -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
@@ -74,8 +87,6 @@ class Redis
74
87
  end
75
88
 
76
89
  attr_accessor :logger
77
- attr_reader :connection
78
- attr_reader :command_map
79
90
 
80
91
  def initialize(options = {})
81
92
  @options = _parse_options(options)
@@ -86,11 +97,14 @@ class Redis
86
97
 
87
98
  @pending_reads = 0
88
99
 
89
- if options.include?(:sentinels)
90
- @connector = Connector::Sentinel.new(@options)
91
- else
92
- @connector = Connector.new(@options)
93
- end
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
94
108
  end
95
109
 
96
110
  def connect
@@ -99,7 +113,34 @@ class Redis
99
113
  # Don't try to reconnect when the connection is fresh
100
114
  with_reconnect(false) do
101
115
  establish_connection
102
- 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]
103
144
  call [:select, db] if db != 0
104
145
  call [:client, :setname, @options[:id]] if @options[:id]
105
146
  @connector.check(self)
@@ -109,7 +150,7 @@ class Redis
109
150
  end
110
151
 
111
152
  def id
112
- @options[:id] || "redis://#{location}/#{db}"
153
+ @options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
113
154
  end
114
155
 
115
156
  def location
@@ -120,7 +161,7 @@ class Redis
120
161
  reply = process([command]) { read }
121
162
  raise reply if reply.is_a?(CommandError)
122
163
 
123
- if block_given?
164
+ if block_given? && reply != 'QUEUED'
124
165
  yield reply
125
166
  else
126
167
  reply
@@ -152,13 +193,16 @@ class Redis
152
193
  end
153
194
 
154
195
  def call_pipeline(pipeline)
196
+ return [] if pipeline.futures.empty?
197
+
155
198
  with_reconnect pipeline.with_reconnect? do
156
199
  begin
157
- pipeline.finish(call_pipelined(pipeline.commands)).tap do
200
+ pipeline.finish(call_pipelined(pipeline)).tap do
158
201
  self.db = pipeline.db if pipeline.db
159
202
  end
160
203
  rescue ConnectionError => e
161
204
  return nil if pipeline.shutdown?
205
+
162
206
  # Assume the pipeline was sent in one piece, but execution of
163
207
  # SHUTDOWN caused none of the replies for commands that were executed
164
208
  # prior to it from coming back around.
@@ -167,8 +211,8 @@ class Redis
167
211
  end
168
212
  end
169
213
 
170
- def call_pipelined(commands)
171
- return [] if commands.empty?
214
+ def call_pipelined(pipeline)
215
+ return [] if pipeline.futures.empty?
172
216
 
173
217
  # The method #ensure_connected (called from #process) reconnects once on
174
218
  # I/O errors. To make an effort in making sure that commands are not
@@ -178,6 +222,8 @@ class Redis
178
222
  # already successfully executed commands. To circumvent this, don't retry
179
223
  # after the first reply has been read successfully.
180
224
 
225
+ commands = pipeline.commands
226
+
181
227
  result = Array.new(commands.size)
182
228
  reconnect = @reconnect
183
229
 
@@ -185,13 +231,14 @@ class Redis
185
231
  exception = nil
186
232
 
187
233
  process(commands) do
188
- result[0] = read
189
-
190
- @reconnect = false
191
-
192
- (commands.size - 1).times do |i|
193
- reply = read
194
- result[i + 1] = reply
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
195
242
  exception = reply if exception.nil? && reply.is_a?(CommandError)
196
243
  end
197
244
  end
@@ -204,7 +251,8 @@ class Redis
204
251
  result
205
252
  end
206
253
 
207
- 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
208
256
  with_socket_timeout(timeout) do
209
257
  call(command, &blk)
210
258
  end
@@ -234,12 +282,13 @@ class Redis
234
282
  end
235
283
 
236
284
  def connected?
237
- !! (connection && connection.connected?)
285
+ !!(connection && connection.connected?)
238
286
  end
239
287
 
240
288
  def disconnect
241
289
  connection.disconnect if connected?
242
290
  end
291
+ alias close disconnect
243
292
 
244
293
  def reconnect
245
294
  disconnect
@@ -274,12 +323,15 @@ class Redis
274
323
 
275
324
  def with_socket_timeout(timeout)
276
325
  connect unless connected?
326
+ original = @options[:read_timeout]
277
327
 
278
328
  begin
279
329
  connection.timeout = timeout
330
+ @options[:read_timeout] = timeout # for reconnection
280
331
  yield
281
332
  ensure
282
333
  connection.timeout = self.timeout if connected?
334
+ @options[:read_timeout] = original
283
335
  end
284
336
  end
285
337
 
@@ -287,30 +339,27 @@ class Redis
287
339
  with_socket_timeout(0, &blk)
288
340
  end
289
341
 
290
- def with_reconnect(val=true)
291
- begin
292
- original, @reconnect = @reconnect, val
293
- yield
294
- ensure
295
- @reconnect = original
296
- end
342
+ def with_reconnect(val = true)
343
+ original, @reconnect = @reconnect, val
344
+ yield
345
+ ensure
346
+ @reconnect = original
297
347
  end
298
348
 
299
349
  def without_reconnect(&blk)
300
350
  with_reconnect(false, &blk)
301
351
  end
302
352
 
303
- protected
353
+ protected
304
354
 
305
355
  def logging(commands)
306
- return yield unless @logger && @logger.debug?
356
+ return yield unless @logger&.debug?
307
357
 
308
358
  begin
309
359
  commands.each do |name, *args|
310
360
  logged_args = args.map do |a|
311
- case
312
- when a.respond_to?(:inspect) then a.inspect
313
- 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
314
363
  else
315
364
  # handle poorly-behaved descendants of BasicObject
316
365
  klass = a.instance_exec { (class << self; self end).superclass }
@@ -336,13 +385,17 @@ class Redis
336
385
  @connection = @options[:driver].connect(@options)
337
386
  @pending_reads = 0
338
387
  rescue TimeoutError,
388
+ SocketError,
389
+ Errno::EADDRNOTAVAIL,
339
390
  Errno::ECONNREFUSED,
340
391
  Errno::EHOSTDOWN,
341
392
  Errno::EHOSTUNREACH,
342
393
  Errno::ENETUNREACH,
343
- Errno::ETIMEDOUT
394
+ Errno::ENOENT,
395
+ Errno::ETIMEDOUT,
396
+ Errno::EINVAL => error
344
397
 
345
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
398
+ raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
346
399
  end
347
400
 
348
401
  def ensure_connected
@@ -356,9 +409,9 @@ class Redis
356
409
  if connected?
357
410
  unless inherit_socket? || Process.pid == @pid
358
411
  raise InheritedError,
359
- "Tried to use a connection from a child process without reconnecting. " +
360
- "You need to reconnect to Redis after forking " +
361
- "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."
362
415
  end
363
416
  else
364
417
  connect
@@ -369,6 +422,10 @@ class Redis
369
422
  disconnect
370
423
 
371
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)
372
429
  retry
373
430
  else
374
431
  raise
@@ -385,17 +442,16 @@ class Redis
385
442
  defaults = DEFAULTS.dup
386
443
  options = options.dup
387
444
 
388
- defaults.keys.each do |key|
445
+ defaults.each_key do |key|
389
446
  # Fill in defaults if needed
390
- if defaults[key].respond_to?(:call)
391
- defaults[key] = defaults[key].call
392
- end
447
+ defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
393
448
 
394
449
  # Symbolize only keys that are needed
395
- 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)
396
451
  end
397
452
 
398
- url = options[:url] || defaults[:url]
453
+ url = options[:url]
454
+ url = defaults[:url] if url.nil?
399
455
 
400
456
  # Override defaults from URL if given
401
457
  if url
@@ -403,13 +459,15 @@ class Redis
403
459
 
404
460
  uri = URI(url)
405
461
 
406
- if uri.scheme == "unix"
407
- defaults[:path] = uri.path
408
- elsif uri.scheme == "redis" || uri.scheme == "rediss"
462
+ case uri.scheme
463
+ when "unix"
464
+ defaults[:path] = uri.path
465
+ when "redis", "rediss"
409
466
  defaults[:scheme] = uri.scheme
410
- defaults[:host] = uri.host if uri.host
467
+ defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
411
468
  defaults[:port] = uri.port if uri.port
412
- 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?
413
471
  defaults[:db] = uri.path[1..-1].to_i if uri.path
414
472
  defaults[:role] = :master
415
473
  else
@@ -420,7 +478,7 @@ class Redis
420
478
  end
421
479
 
422
480
  # Use default when option is not specified or nil
423
- defaults.keys.each do |key|
481
+ defaults.each_key do |key|
424
482
  options[key] = defaults[key] if options[key].nil?
425
483
  end
426
484
 
@@ -435,7 +493,7 @@ class Redis
435
493
  options[:port] = options[:port].to_i
436
494
  end
437
495
 
438
- if options.has_key?(:timeout)
496
+ if options.key?(:timeout)
439
497
  options[:connect_timeout] ||= options[:timeout]
440
498
  options[:read_timeout] ||= options[:timeout]
441
499
  options[:write_timeout] ||= options[:timeout]
@@ -445,12 +503,16 @@ class Redis
445
503
  options[:read_timeout] = Float(options[:read_timeout])
446
504
  options[:write_timeout] = Float(options[:write_timeout])
447
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
+
448
510
  options[:db] = options[:db].to_i
449
511
  options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
450
512
 
451
513
  case options[:tcp_keepalive]
452
514
  when Hash
453
- [:time, :intvl, :probes].each do |key|
515
+ %i[time intvl probes].each do |key|
454
516
  unless options[:tcp_keepalive][key].is_a?(Integer)
455
517
  raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
456
518
  end
@@ -458,13 +520,13 @@ class Redis
458
520
 
459
521
  when Integer
460
522
  if options[:tcp_keepalive] >= 60
461
- 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 }
462
524
 
463
525
  elsif options[:tcp_keepalive] >= 30
464
- 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 }
465
527
 
466
528
  elsif options[:tcp_keepalive] >= 5
467
- 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 }
468
530
  end
469
531
  end
470
532
 
@@ -476,13 +538,18 @@ class Redis
476
538
  def _parse_driver(driver)
477
539
  driver = driver.to_s if driver.is_a?(Symbol)
478
540
 
479
- if driver.kind_of?(String)
541
+ if driver.is_a?(String)
480
542
  begin
481
- require "redis/connection/#{driver}"
482
- driver = Connection.const_get(driver.capitalize)
543
+ require_relative "connection/#{driver}"
483
544
  rescue LoadError, NameError
484
- 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
485
550
  end
551
+
552
+ driver = Connection.const_get(driver.capitalize)
486
553
  end
487
554
 
488
555
  driver
@@ -497,18 +564,16 @@ class Redis
497
564
  @options
498
565
  end
499
566
 
500
- def check(client)
501
- end
567
+ def check(client); end
502
568
 
503
569
  class Sentinel < Connector
504
570
  def initialize(options)
505
571
  super(options)
506
572
 
507
- @options[:password] = DEFAULTS.fetch(:password)
508
573
  @options[:db] = DEFAULTS.fetch(:db)
509
574
 
510
575
  @sentinels = @options.delete(:sentinels).dup
511
- @role = @options.fetch(:role, "master").to_s
576
+ @role = (@options[:role] || "master").to_s
512
577
  @master = @options[:host]
513
578
  end
514
579
 
@@ -531,13 +596,13 @@ class Redis
531
596
 
532
597
  def resolve
533
598
  result = case @role
534
- when "master"
535
- resolve_master
536
- when "slave"
537
- resolve_slave
538
- else
539
- raise ArgumentError, "Unknown instance role #{@role}"
540
- end
599
+ when "master"
600
+ resolve_master
601
+ when "slave"
602
+ resolve_slave
603
+ else
604
+ raise ArgumentError, "Unknown instance role #{@role}"
605
+ end
541
606
 
542
607
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
543
608
  end
@@ -545,10 +610,12 @@ class Redis
545
610
  def sentinel_detect
546
611
  @sentinels.each do |sentinel|
547
612
  client = Client.new(@options.merge({
548
- :host => sentinel[:host],
549
- :port => sentinel[:port],
550
- :reconnect_attempts => 0,
551
- }))
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
+ }))
552
619
 
553
620
  begin
554
621
  if result = yield(client)
@@ -570,7 +637,7 @@ class Redis
570
637
  def resolve_master
571
638
  sentinel_detect do |client|
572
639
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
573
- {:host => reply[0], :port => reply[1]}
640
+ { host: reply[0], port: reply[1] }
574
641
  end
575
642
  end
576
643
  end
@@ -578,9 +645,19 @@ class Redis
578
645
  def resolve_slave
579
646
  sentinel_detect do |client|
580
647
  if reply = client.call(["sentinel", "slaves", @master])
581
- slave = Hash[*reply.sample]
582
-
583
- {: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
584
661
  end
585
662
  end
586
663
  end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../errors'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Keep details about Redis commands for Redis Cluster Client.
8
+ # @see https://redis.io/commands/command
9
+ class Command
10
+ def initialize(details)
11
+ @details = pick_details(details)
12
+ end
13
+
14
+ def extract_first_key(command)
15
+ i = determine_first_key_position(command)
16
+ return '' if i == 0
17
+
18
+ key = command[i].to_s
19
+ hash_tag = extract_hash_tag(key)
20
+ hash_tag.empty? ? key : hash_tag
21
+ end
22
+
23
+ def should_send_to_master?(command)
24
+ dig_details(command, :write)
25
+ end
26
+
27
+ def should_send_to_slave?(command)
28
+ dig_details(command, :readonly)
29
+ end
30
+
31
+ private
32
+
33
+ def pick_details(details)
34
+ details.transform_values do |detail|
35
+ {
36
+ first_key_position: detail[:first],
37
+ write: detail[:flags].include?('write'),
38
+ readonly: detail[:flags].include?('readonly')
39
+ }
40
+ end
41
+ end
42
+
43
+ def dig_details(command, key)
44
+ name = command.first.to_s
45
+ return unless @details.key?(name)
46
+
47
+ @details.fetch(name).fetch(key)
48
+ end
49
+
50
+ def determine_first_key_position(command)
51
+ case command.first.to_s.downcase
52
+ when 'eval', 'evalsha', 'migrate', 'zinterstore', 'zunionstore' then 3
53
+ when 'object' then 2
54
+ when 'memory'
55
+ command[1].to_s.casecmp('usage').zero? ? 2 : 0
56
+ when 'xread', 'xreadgroup'
57
+ determine_optional_key_position(command, 'streams')
58
+ else
59
+ dig_details(command, :first_key_position).to_i
60
+ end
61
+ end
62
+
63
+ def determine_optional_key_position(command, option_name)
64
+ idx = command.map(&:to_s).map(&:downcase).index(option_name)
65
+ idx.nil? ? 0 : idx + 1
66
+ end
67
+
68
+ # @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
69
+ def extract_hash_tag(key)
70
+ s = key.index('{')
71
+ e = key.index('}', s.to_i + 1)
72
+
73
+ return '' if s.nil? || e.nil?
74
+
75
+ key[s + 1..e - 1]
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'redis/errors'
4
+
5
+ class Redis
6
+ class Cluster
7
+ # Load details about Redis commands for Redis Cluster Client
8
+ # @see https://redis.io/commands/command
9
+ module CommandLoader
10
+ module_function
11
+
12
+ def load(nodes)
13
+ errors = nodes.map do |node|
14
+ begin
15
+ return fetch_command_details(node)
16
+ rescue CannotConnectError, ConnectionError, CommandError => error
17
+ error
18
+ end
19
+ end
20
+
21
+ raise InitialSetupError, errors
22
+ end
23
+
24
+ def fetch_command_details(node)
25
+ node.call(%i[command]).map do |reply|
26
+ [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
27
+ end.to_h
28
+ end
29
+
30
+ private_class_method :fetch_command_details
31
+ end
32
+ end
33
+ end