redis 3.3.5 → 4.8.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (147) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +236 -2
  3. data/README.md +169 -89
  4. data/lib/redis/client.rb +176 -108
  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 +7 -10
  33. data/lib/redis/connection/hiredis.rb +5 -3
  34. data/lib/redis/connection/registry.rb +2 -1
  35. data/lib/redis/connection/ruby.rb +136 -128
  36. data/lib/redis/connection/synchrony.rb +24 -9
  37. data/lib/redis/connection.rb +3 -1
  38. data/lib/redis/distributed.rb +255 -85
  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 +174 -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
@@ -253,7 +302,7 @@ class Redis
253
302
  e2 = TimeoutError.new("Connection timed out")
254
303
  e2.set_backtrace(e1.backtrace)
255
304
  raise e2
256
- 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
257
306
  raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
258
307
  end
259
308
 
@@ -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,39 +385,38 @@ 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
349
- disconnect if @pending_reads > 0
402
+ disconnect if @pending_reads > 0 || (@pid != Process.pid && !inherit_socket?)
350
403
 
351
404
  attempts = 0
352
405
 
353
406
  begin
354
407
  attempts += 1
355
408
 
356
- if connected?
357
- unless inherit_socket? || Process.pid == @pid
358
- 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."
362
- end
363
- else
364
- connect
365
- end
409
+ connect unless connected?
366
410
 
367
411
  yield
368
412
  rescue BaseConnectionError
369
413
  disconnect
370
414
 
371
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)
372
420
  retry
373
421
  else
374
422
  raise
@@ -385,17 +433,16 @@ class Redis
385
433
  defaults = DEFAULTS.dup
386
434
  options = options.dup
387
435
 
388
- defaults.keys.each do |key|
436
+ defaults.each_key do |key|
389
437
  # Fill in defaults if needed
390
- if defaults[key].respond_to?(:call)
391
- defaults[key] = defaults[key].call
392
- end
438
+ defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
393
439
 
394
440
  # Symbolize only keys that are needed
395
- 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)
396
442
  end
397
443
 
398
- url = options[:url] || defaults[:url]
444
+ url = options[:url]
445
+ url = defaults[:url] if url.nil?
399
446
 
400
447
  # Override defaults from URL if given
401
448
  if url
@@ -403,13 +450,15 @@ class Redis
403
450
 
404
451
  uri = URI(url)
405
452
 
406
- if uri.scheme == "unix"
407
- defaults[:path] = uri.path
408
- elsif uri.scheme == "redis" || uri.scheme == "rediss"
453
+ case uri.scheme
454
+ when "unix"
455
+ defaults[:path] = uri.path
456
+ when "redis", "rediss"
409
457
  defaults[:scheme] = uri.scheme
410
- defaults[:host] = uri.host if uri.host
458
+ defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
411
459
  defaults[:port] = uri.port if uri.port
412
- 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?
413
462
  defaults[:db] = uri.path[1..-1].to_i if uri.path
414
463
  defaults[:role] = :master
415
464
  else
@@ -420,7 +469,7 @@ class Redis
420
469
  end
421
470
 
422
471
  # Use default when option is not specified or nil
423
- defaults.keys.each do |key|
472
+ defaults.each_key do |key|
424
473
  options[key] = defaults[key] if options[key].nil?
425
474
  end
426
475
 
@@ -435,7 +484,7 @@ class Redis
435
484
  options[:port] = options[:port].to_i
436
485
  end
437
486
 
438
- if options.has_key?(:timeout)
487
+ if options.key?(:timeout)
439
488
  options[:connect_timeout] ||= options[:timeout]
440
489
  options[:read_timeout] ||= options[:timeout]
441
490
  options[:write_timeout] ||= options[:timeout]
@@ -445,12 +494,16 @@ class Redis
445
494
  options[:read_timeout] = Float(options[:read_timeout])
446
495
  options[:write_timeout] = Float(options[:write_timeout])
447
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
+
448
501
  options[:db] = options[:db].to_i
449
502
  options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
450
503
 
451
504
  case options[:tcp_keepalive]
452
505
  when Hash
453
- [:time, :intvl, :probes].each do |key|
506
+ %i[time intvl probes].each do |key|
454
507
  unless options[:tcp_keepalive][key].is_a?(Integer)
455
508
  raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
456
509
  end
@@ -458,13 +511,13 @@ class Redis
458
511
 
459
512
  when Integer
460
513
  if options[:tcp_keepalive] >= 60
461
- 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 }
462
515
 
463
516
  elsif options[:tcp_keepalive] >= 30
464
- 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 }
465
518
 
466
519
  elsif options[:tcp_keepalive] >= 5
467
- 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 }
468
521
  end
469
522
  end
470
523
 
@@ -476,13 +529,18 @@ class Redis
476
529
  def _parse_driver(driver)
477
530
  driver = driver.to_s if driver.is_a?(Symbol)
478
531
 
479
- if driver.kind_of?(String)
532
+ if driver.is_a?(String)
480
533
  begin
481
- require "redis/connection/#{driver}"
482
- driver = Connection.const_get(driver.capitalize)
534
+ require_relative "connection/#{driver}"
483
535
  rescue LoadError, NameError
484
- raise RuntimeError, "Cannot load driver #{driver.inspect}"
536
+ begin
537
+ require "redis/connection/#{driver}"
538
+ rescue LoadError, NameError => error
539
+ raise "Cannot load driver #{driver.inspect}: #{error.message}"
540
+ end
485
541
  end
542
+
543
+ driver = Connection.const_get(driver.capitalize)
486
544
  end
487
545
 
488
546
  driver
@@ -497,18 +555,16 @@ class Redis
497
555
  @options
498
556
  end
499
557
 
500
- def check(client)
501
- end
558
+ def check(client); end
502
559
 
503
560
  class Sentinel < Connector
504
561
  def initialize(options)
505
562
  super(options)
506
563
 
507
- @options[:password] = DEFAULTS.fetch(:password)
508
564
  @options[:db] = DEFAULTS.fetch(:db)
509
565
 
510
566
  @sentinels = @options.delete(:sentinels).dup
511
- @role = @options.fetch(:role, "master").to_s
567
+ @role = (@options[:role] || "master").to_s
512
568
  @master = @options[:host]
513
569
  end
514
570
 
@@ -531,13 +587,13 @@ class Redis
531
587
 
532
588
  def resolve
533
589
  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
590
+ when "master"
591
+ resolve_master
592
+ when "slave"
593
+ resolve_slave
594
+ else
595
+ raise ArgumentError, "Unknown instance role #{@role}"
596
+ end
541
597
 
542
598
  result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
543
599
  end
@@ -545,10 +601,12 @@ class Redis
545
601
  def sentinel_detect
546
602
  @sentinels.each do |sentinel|
547
603
  client = Client.new(@options.merge({
548
- :host => sentinel[:host],
549
- :port => sentinel[:port],
550
- :reconnect_attempts => 0,
551
- }))
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
+ }))
552
610
 
553
611
  begin
554
612
  if result = yield(client)
@@ -570,7 +628,7 @@ class Redis
570
628
  def resolve_master
571
629
  sentinel_detect do |client|
572
630
  if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
573
- {:host => reply[0], :port => reply[1]}
631
+ { host: reply[0], port: reply[1] }
574
632
  end
575
633
  end
576
634
  end
@@ -578,9 +636,19 @@ class Redis
578
636
  def resolve_slave
579
637
  sentinel_detect do |client|
580
638
  if reply = client.call(["sentinel", "slaves", @master])
581
- slave = Hash[*reply.sample]
582
-
583
- {: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
584
652
  end
585
653
  end
586
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