redis 4.0.1 → 4.8.1

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