redis 4.5.1 → 5.0.6

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +120 -0
  3. data/README.md +82 -153
  4. data/lib/redis/client.rb +77 -615
  5. data/lib/redis/commands/bitmaps.rb +66 -0
  6. data/lib/redis/commands/cluster.rb +28 -0
  7. data/lib/redis/commands/connection.rb +53 -0
  8. data/lib/redis/commands/geo.rb +84 -0
  9. data/lib/redis/commands/hashes.rb +254 -0
  10. data/lib/redis/commands/hyper_log_log.rb +37 -0
  11. data/lib/redis/commands/keys.rb +437 -0
  12. data/lib/redis/commands/lists.rb +285 -0
  13. data/lib/redis/commands/pubsub.rb +54 -0
  14. data/lib/redis/commands/scripting.rb +114 -0
  15. data/lib/redis/commands/server.rb +188 -0
  16. data/lib/redis/commands/sets.rb +214 -0
  17. data/lib/redis/commands/sorted_sets.rb +818 -0
  18. data/lib/redis/commands/streams.rb +402 -0
  19. data/lib/redis/commands/strings.rb +314 -0
  20. data/lib/redis/commands/transactions.rb +115 -0
  21. data/lib/redis/commands.rb +237 -0
  22. data/lib/redis/distributed.rb +140 -70
  23. data/lib/redis/errors.rb +15 -41
  24. data/lib/redis/hash_ring.rb +26 -26
  25. data/lib/redis/pipeline.rb +66 -120
  26. data/lib/redis/subscribe.rb +23 -15
  27. data/lib/redis/version.rb +1 -1
  28. data/lib/redis.rb +106 -3736
  29. metadata +26 -53
  30. data/lib/redis/cluster/command.rb +0 -81
  31. data/lib/redis/cluster/command_loader.rb +0 -33
  32. data/lib/redis/cluster/key_slot_converter.rb +0 -72
  33. data/lib/redis/cluster/node.rb +0 -108
  34. data/lib/redis/cluster/node_key.rb +0 -31
  35. data/lib/redis/cluster/node_loader.rb +0 -37
  36. data/lib/redis/cluster/option.rb +0 -93
  37. data/lib/redis/cluster/slot.rb +0 -86
  38. data/lib/redis/cluster/slot_loader.rb +0 -49
  39. data/lib/redis/cluster.rb +0 -291
  40. data/lib/redis/connection/command_helper.rb +0 -41
  41. data/lib/redis/connection/hiredis.rb +0 -67
  42. data/lib/redis/connection/registry.rb +0 -13
  43. data/lib/redis/connection/ruby.rb +0 -428
  44. data/lib/redis/connection/synchrony.rb +0 -146
  45. data/lib/redis/connection.rb +0 -11
data/lib/redis/client.rb CHANGED
@@ -1,665 +1,127 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "errors"
4
- require "socket"
5
- require "cgi"
3
+ require 'redis-client'
6
4
 
7
5
  class Redis
8
- class Client
9
- # Defaults are also used for converting string keys to symbols.
10
- DEFAULTS = {
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
6
+ class Client < ::RedisClient
7
+ ERROR_MAPPING = {
8
+ RedisClient::ConnectionError => Redis::ConnectionError,
9
+ RedisClient::CommandError => Redis::CommandError,
10
+ RedisClient::ReadTimeoutError => Redis::TimeoutError,
11
+ RedisClient::CannotConnectError => Redis::CannotConnectError,
12
+ RedisClient::AuthenticationError => Redis::CannotConnectError,
13
+ RedisClient::FailoverError => Redis::CannotConnectError,
14
+ RedisClient::PermissionError => Redis::PermissionError,
15
+ RedisClient::WrongTypeError => Redis::WrongTypeError,
16
+ RedisClient::ReadOnlyError => Redis::ReadOnlyError,
17
+ RedisClient::ProtocolError => Redis::ProtocolError,
18
+ RedisClient::OutOfMemoryError => Redis::OutOfMemoryError,
19
+ }
34
20
 
35
- attr_reader :options
36
-
37
- def scheme
38
- @options[:scheme]
39
- end
40
-
41
- def host
42
- @options[:host]
43
- end
44
-
45
- def port
46
- @options[:port]
47
- end
48
-
49
- def path
50
- @options[:path]
51
- end
52
-
53
- def read_timeout
54
- @options[:read_timeout]
55
- end
56
-
57
- def connect_timeout
58
- @options[:connect_timeout]
59
- end
60
-
61
- def timeout
62
- @options[:read_timeout]
63
- end
64
-
65
- def username
66
- @options[:username]
67
- end
68
-
69
- def password
70
- @options[:password]
71
- end
72
-
73
- def db
74
- @options[:db]
75
- end
76
-
77
- def db=(db)
78
- @options[:db] = db.to_i
79
- end
80
-
81
- def driver
82
- @options[:driver]
83
- end
84
-
85
- def inherit_socket?
86
- @options[:inherit_socket]
87
- end
88
-
89
- attr_accessor :logger
90
- attr_reader :connection
91
- attr_reader :command_map
92
-
93
- def initialize(options = {})
94
- @options = _parse_options(options)
95
- @reconnect = true
96
- @logger = @options[:logger]
97
- @connection = nil
98
- @command_map = {}
99
-
100
- @pending_reads = 0
101
-
102
- @connector =
103
- if !@options[:sentinels].nil?
104
- Connector::Sentinel.new(@options)
105
- elsif options.include?(:connector) && options[:connector].respond_to?(:new)
106
- options.delete(:connector).new(@options)
107
- else
108
- Connector.new(@options)
109
- end
110
- end
111
-
112
- def connect
113
- @pid = Process.pid
114
-
115
- # Don't try to reconnect when the connection is fresh
116
- with_reconnect(false) do
117
- establish_connection
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
- elsif err.message.match?(/WRONGPASS invalid username-password pair/)
126
- begin
127
- call [:auth, password]
128
- rescue CommandError
129
- raise err
130
- end
131
- ::Kernel.warn(
132
- "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
133
- " the provided password was for the default user. This will start failing in redis-rb 4.6."
134
- )
135
- else
136
- raise
137
- end
138
- end
139
- else
140
- call [:auth, password]
141
- end
142
- end
143
-
144
- call [:readonly] if @options[:readonly]
145
- call [:select, db] if db != 0
146
- call [:client, :setname, @options[:id]] if @options[:id]
147
- @connector.check(self)
21
+ class << self
22
+ def config(**kwargs)
23
+ super(protocol: 2, **kwargs)
148
24
  end
149
25
 
150
- self
151
- end
152
-
153
- def id
154
- @options[:id] || "redis://#{location}/#{db}"
155
- end
156
-
157
- def location
158
- path || "#{host}:#{port}"
159
- end
160
-
161
- def call(command)
162
- reply = process([command]) { read }
163
- raise reply if reply.is_a?(CommandError)
164
-
165
- if block_given? && reply != 'QUEUED'
166
- yield reply
167
- else
168
- reply
26
+ def sentinel(**kwargs)
27
+ super(protocol: 2, **kwargs)
169
28
  end
170
29
  end
171
30
 
172
- def call_loop(command, timeout = 0)
173
- error = nil
174
-
175
- result = with_socket_timeout(timeout) do
176
- process([command]) do
177
- loop do
178
- reply = read
179
- if reply.is_a?(CommandError)
180
- error = reply
181
- break
182
- else
183
- yield reply
184
- end
185
- end
186
- end
187
- end
188
-
189
- # Raise error when previous block broke out of the loop.
190
- raise error if error
191
-
192
- # Result is set to the value that the provided block used to break.
193
- result
194
- end
195
-
196
- def call_pipeline(pipeline)
197
- return [] if pipeline.futures.empty?
198
-
199
- with_reconnect pipeline.with_reconnect? do
200
- begin
201
- pipeline.finish(call_pipelined(pipeline)).tap do
202
- self.db = pipeline.db if pipeline.db
203
- end
204
- rescue ConnectionError => e
205
- return nil if pipeline.shutdown?
206
-
207
- # Assume the pipeline was sent in one piece, but execution of
208
- # SHUTDOWN caused none of the replies for commands that were executed
209
- # prior to it from coming back around.
210
- raise e
211
- end
212
- end
31
+ def id
32
+ config.id
213
33
  end
214
34
 
215
- def call_pipelined(pipeline)
216
- return [] if pipeline.futures.empty?
217
-
218
- # The method #ensure_connected (called from #process) reconnects once on
219
- # I/O errors. To make an effort in making sure that commands are not
220
- # executed more than once, only allow reconnection before the first reply
221
- # has been read. When an error occurs after the first reply has been
222
- # read, retrying would re-execute the entire pipeline, thus re-issuing
223
- # already successfully executed commands. To circumvent this, don't retry
224
- # after the first reply has been read successfully.
225
-
226
- commands = pipeline.commands
227
-
228
- result = Array.new(commands.size)
229
- reconnect = @reconnect
230
-
231
- begin
232
- exception = nil
233
-
234
- process(commands) do
235
- pipeline.timeouts.each_with_index do |timeout, i|
236
- reply = if timeout
237
- with_socket_timeout(timeout) { read }
238
- else
239
- read
240
- end
241
- result[i] = reply
242
- @reconnect = false
243
- exception = reply if exception.nil? && reply.is_a?(CommandError)
244
- end
245
- end
246
-
247
- raise exception if exception
248
- ensure
249
- @reconnect = reconnect
250
- end
251
-
252
- result
35
+ def server_url
36
+ config.server_url
253
37
  end
254
38
 
255
- def call_with_timeout(command, timeout, &blk)
256
- with_socket_timeout(timeout) do
257
- call(command, &blk)
258
- end
259
- rescue ConnectionError
260
- retry
39
+ def timeout
40
+ config.read_timeout
261
41
  end
262
42
 
263
- def call_without_timeout(command, &blk)
264
- call_with_timeout(command, 0, &blk)
43
+ def db
44
+ config.db
265
45
  end
266
46
 
267
- def process(commands)
268
- logging(commands) do
269
- ensure_connected do
270
- commands.each do |command|
271
- if command_map[command.first]
272
- command = command.dup
273
- command[0] = command_map[command.first]
274
- end
275
-
276
- write(command)
277
- end
278
-
279
- yield if block_given?
280
- end
281
- end
47
+ def host
48
+ config.host unless config.path
282
49
  end
283
50
 
284
- def connected?
285
- !!(connection && connection.connected?)
51
+ def port
52
+ config.port unless config.path
286
53
  end
287
54
 
288
- def disconnect
289
- connection.disconnect if connected?
55
+ def path
56
+ config.path
290
57
  end
291
- alias close disconnect
292
58
 
293
- def reconnect
294
- disconnect
295
- connect
59
+ def username
60
+ config.username
296
61
  end
297
62
 
298
- def io
299
- yield
300
- rescue TimeoutError => e1
301
- # Add a message to the exception without destroying the original stack
302
- e2 = TimeoutError.new("Connection timed out")
303
- e2.set_backtrace(e1.backtrace)
304
- raise e2
305
- rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
306
- raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
63
+ def password
64
+ config.password
307
65
  end
308
66
 
309
- def read
310
- io do
311
- value = connection.read
312
- @pending_reads -= 1
313
- value
314
- end
315
- end
67
+ undef_method :call
68
+ undef_method :call_once
69
+ undef_method :call_once_v
70
+ undef_method :blocking_call
316
71
 
317
- def write(command)
318
- io do
319
- @pending_reads += 1
320
- connection.write(command)
321
- end
72
+ def call_v(command, &block)
73
+ super(command, &block)
74
+ rescue ::RedisClient::Error => error
75
+ translate_error!(error)
322
76
  end
323
77
 
324
- def with_socket_timeout(timeout)
325
- connect unless connected?
326
- original = @options[:read_timeout]
327
-
328
- begin
329
- connection.timeout = timeout
330
- @options[:read_timeout] = timeout # for reconnection
331
- yield
332
- ensure
333
- connection.timeout = self.timeout if connected?
334
- @options[:read_timeout] = original
78
+ def blocking_call_v(timeout, command, &block)
79
+ if timeout && timeout > 0
80
+ # Can't use the command timeout argument as the connection timeout
81
+ # otherwise it would be very racy. So we add the regular read_timeout on top
82
+ # to account for the network delay.
83
+ timeout += config.read_timeout
335
84
  end
336
- end
337
85
 
338
- def without_socket_timeout(&blk)
339
- with_socket_timeout(0, &blk)
86
+ super(timeout, command, &block)
87
+ rescue ::RedisClient::Error => error
88
+ translate_error!(error)
340
89
  end
341
90
 
342
- def with_reconnect(val = true)
343
- original, @reconnect = @reconnect, val
344
- yield
345
- ensure
346
- @reconnect = original
91
+ def pipelined
92
+ super
93
+ rescue ::RedisClient::Error => error
94
+ translate_error!(error)
347
95
  end
348
96
 
349
- def without_reconnect(&blk)
350
- with_reconnect(false, &blk)
97
+ def multi
98
+ super
99
+ rescue ::RedisClient::Error => error
100
+ translate_error!(error)
351
101
  end
352
102
 
353
- protected
354
-
355
- def logging(commands)
356
- return yield unless @logger&.debug?
357
-
358
- begin
359
- commands.each do |name, *args|
360
- logged_args = args.map do |a|
361
- if a.respond_to?(:inspect) then a.inspect
362
- elsif a.respond_to?(:to_s) then a.to_s
363
- else
364
- # handle poorly-behaved descendants of BasicObject
365
- klass = a.instance_exec { (class << self; self end).superclass }
366
- "\#<#{klass}:#{a.__id__}>"
367
- end
368
- end
369
- @logger.debug("[Redis] command=#{name.to_s.upcase} args=#{logged_args.join(' ')}")
370
- end
371
-
372
- t1 = Time.now
373
- yield
374
- ensure
375
- @logger.debug("[Redis] call_time=%0.2f ms" % ((Time.now - t1) * 1000)) if t1
376
- end
103
+ def disable_reconnection(&block)
104
+ ensure_connected(retryable: false, &block)
377
105
  end
378
106
 
379
- def establish_connection
380
- server = @connector.resolve.dup
381
-
382
- @options[:host] = server[:host]
383
- @options[:port] = Integer(server[:port]) if server.include?(:port)
384
-
385
- @connection = @options[:driver].connect(@options)
386
- @pending_reads = 0
387
- rescue TimeoutError,
388
- SocketError,
389
- Errno::EADDRNOTAVAIL,
390
- Errno::ECONNREFUSED,
391
- Errno::EHOSTDOWN,
392
- Errno::EHOSTUNREACH,
393
- Errno::ENETUNREACH,
394
- Errno::ENOENT,
395
- Errno::ETIMEDOUT,
396
- Errno::EINVAL => error
397
-
398
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{error.class})"
107
+ def inherit_socket!
108
+ @inherit_socket = true
399
109
  end
400
110
 
401
- def ensure_connected
402
- disconnect if @pending_reads > 0
403
-
404
- attempts = 0
405
-
406
- begin
407
- attempts += 1
408
-
409
- if connected?
410
- unless inherit_socket? || Process.pid == @pid
411
- raise InheritedError,
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."
415
- end
416
- else
417
- connect
418
- end
419
-
420
- yield
421
- rescue BaseConnectionError
422
- disconnect
111
+ private
423
112
 
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)
429
- retry
430
- else
431
- raise
432
- end
433
- rescue Exception
434
- disconnect
435
- raise
436
- end
113
+ def translate_error!(error)
114
+ redis_error = translate_error_class(error.class)
115
+ raise redis_error, error.message, error.backtrace
437
116
  end
438
117
 
439
- def _parse_options(options)
440
- return options if options[:_parsed]
441
-
442
- defaults = DEFAULTS.dup
443
- options = options.dup
444
-
445
- defaults.keys.each do |key|
446
- # Fill in defaults if needed
447
- defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
448
-
449
- # Symbolize only keys that are needed
450
- options[key] = options[key.to_s] if options.key?(key.to_s)
451
- end
452
-
453
- url = options[:url]
454
- url = defaults[:url] if url.nil?
455
-
456
- # Override defaults from URL if given
457
- if url
458
- require "uri"
459
-
460
- uri = URI(url)
461
-
462
- if uri.scheme == "unix"
463
- defaults[:path] = uri.path
464
- elsif uri.scheme == "redis" || uri.scheme == "rediss"
465
- defaults[:scheme] = uri.scheme
466
- defaults[:host] = uri.host if uri.host
467
- defaults[:port] = uri.port if uri.port
468
- defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
469
- defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
470
- defaults[:db] = uri.path[1..-1].to_i if uri.path
471
- defaults[:role] = :master
472
- else
473
- raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
474
- end
475
-
476
- defaults[:ssl] = true if uri.scheme == "rediss"
477
- end
478
-
479
- # Use default when option is not specified or nil
480
- defaults.keys.each do |key|
481
- options[key] = defaults[key] if options[key].nil?
482
- end
483
-
484
- if options[:path]
485
- # Unix socket
486
- options[:scheme] = "unix"
487
- options.delete(:host)
488
- options.delete(:port)
118
+ def translate_error_class(error_class)
119
+ ERROR_MAPPING.fetch(error_class)
120
+ rescue IndexError
121
+ if (client_error = error_class.ancestors.find { |a| ERROR_MAPPING[a] })
122
+ ERROR_MAPPING[error_class] = ERROR_MAPPING[client_error]
489
123
  else
490
- # TCP socket
491
- options[:host] = options[:host].to_s
492
- options[:port] = options[:port].to_i
493
- end
494
-
495
- if options.key?(:timeout)
496
- options[:connect_timeout] ||= options[:timeout]
497
- options[:read_timeout] ||= options[:timeout]
498
- options[:write_timeout] ||= options[:timeout]
499
- end
500
-
501
- options[:connect_timeout] = Float(options[:connect_timeout])
502
- options[:read_timeout] = Float(options[:read_timeout])
503
- options[:write_timeout] = Float(options[:write_timeout])
504
-
505
- options[:reconnect_attempts] = options[:reconnect_attempts].to_i
506
- options[:reconnect_delay] = options[:reconnect_delay].to_f
507
- options[:reconnect_delay_max] = options[:reconnect_delay_max].to_f
508
-
509
- options[:db] = options[:db].to_i
510
- options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
511
-
512
- case options[:tcp_keepalive]
513
- when Hash
514
- %i[time intvl probes].each do |key|
515
- unless options[:tcp_keepalive][key].is_a?(Integer)
516
- raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
517
- end
518
- end
519
-
520
- when Integer
521
- if options[:tcp_keepalive] >= 60
522
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
523
-
524
- elsif options[:tcp_keepalive] >= 30
525
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
526
-
527
- elsif options[:tcp_keepalive] >= 5
528
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
529
- end
530
- end
531
-
532
- options[:_parsed] = true
533
-
534
- options
535
- end
536
-
537
- def _parse_driver(driver)
538
- driver = driver.to_s if driver.is_a?(Symbol)
539
-
540
- if driver.is_a?(String)
541
- begin
542
- require_relative "connection/#{driver}"
543
- rescue LoadError, NameError
544
- begin
545
- require "redis/connection/#{driver}"
546
- rescue LoadError, NameError => error
547
- raise "Cannot load driver #{driver.inspect}: #{error.message}"
548
- end
549
- end
550
-
551
- driver = Connection.const_get(driver.capitalize)
552
- end
553
-
554
- driver
555
- end
556
-
557
- class Connector
558
- def initialize(options)
559
- @options = options.dup
560
- end
561
-
562
- def resolve
563
- @options
564
- end
565
-
566
- def check(client); end
567
-
568
- class Sentinel < Connector
569
- def initialize(options)
570
- super(options)
571
-
572
- @options[:db] = DEFAULTS.fetch(:db)
573
-
574
- @sentinels = @options.delete(:sentinels).dup
575
- @role = (@options[:role] || "master").to_s
576
- @master = @options[:host]
577
- end
578
-
579
- def check(client)
580
- # Check the instance is really of the role we are looking for.
581
- # We can't assume the command is supported since it was introduced
582
- # recently and this client should work with old stuff.
583
- begin
584
- role = client.call([:role])[0]
585
- rescue Redis::CommandError
586
- # Assume the test is passed if we can't get a reply from ROLE...
587
- role = @role
588
- end
589
-
590
- if role != @role
591
- client.disconnect
592
- raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
593
- end
594
- end
595
-
596
- def resolve
597
- result = case @role
598
- when "master"
599
- resolve_master
600
- when "slave"
601
- resolve_slave
602
- else
603
- raise ArgumentError, "Unknown instance role #{@role}"
604
- end
605
-
606
- result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
607
- end
608
-
609
- def sentinel_detect
610
- @sentinels.each do |sentinel|
611
- client = Client.new(@options.merge({
612
- host: sentinel[:host] || sentinel["host"],
613
- port: sentinel[:port] || sentinel["port"],
614
- username: sentinel[:username] || sentinel["username"],
615
- password: sentinel[:password] || sentinel["password"],
616
- reconnect_attempts: 0
617
- }))
618
-
619
- begin
620
- if result = yield(client)
621
- # This sentinel responded. Make sure we ask it first next time.
622
- @sentinels.delete(sentinel)
623
- @sentinels.unshift(sentinel)
624
-
625
- return result
626
- end
627
- rescue BaseConnectionError
628
- ensure
629
- client.disconnect
630
- end
631
- end
632
-
633
- raise CannotConnectError, "No sentinels available."
634
- end
635
-
636
- def resolve_master
637
- sentinel_detect do |client|
638
- if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
639
- { host: reply[0], port: reply[1] }
640
- end
641
- end
642
- end
643
-
644
- def resolve_slave
645
- sentinel_detect do |client|
646
- if reply = client.call(["sentinel", "slaves", @master])
647
- slaves = reply.map { |s| s.each_slice(2).to_h }
648
- slaves.each { |s| s['flags'] = s.fetch('flags').split(',') }
649
- slaves.reject! { |s| s.fetch('flags').include?('s_down') }
650
-
651
- if slaves.empty?
652
- raise CannotConnectError, 'No slaves available.'
653
- else
654
- slave = slaves.sample
655
- {
656
- host: slave.fetch('ip'),
657
- port: slave.fetch('port')
658
- }
659
- end
660
- end
661
- end
662
- end
124
+ raise
663
125
  end
664
126
  end
665
127
  end