redis 4.7.1 → 5.0.0.beta3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -0
  3. data/README.md +77 -161
  4. data/lib/redis/client.rb +78 -615
  5. data/lib/redis/commands/bitmaps.rb +4 -1
  6. data/lib/redis/commands/cluster.rb +1 -18
  7. data/lib/redis/commands/connection.rb +5 -10
  8. data/lib/redis/commands/hashes.rb +6 -3
  9. data/lib/redis/commands/hyper_log_log.rb +1 -1
  10. data/lib/redis/commands/keys.rb +52 -26
  11. data/lib/redis/commands/lists.rb +10 -14
  12. data/lib/redis/commands/pubsub.rb +7 -9
  13. data/lib/redis/commands/server.rb +14 -14
  14. data/lib/redis/commands/sets.rb +42 -35
  15. data/lib/redis/commands/sorted_sets.rb +4 -2
  16. data/lib/redis/commands/streams.rb +12 -10
  17. data/lib/redis/commands/strings.rb +1 -0
  18. data/lib/redis/commands/transactions.rb +7 -31
  19. data/lib/redis/commands.rb +1 -8
  20. data/lib/redis/distributed.rb +99 -66
  21. data/lib/redis/errors.rb +10 -52
  22. data/lib/redis/hash_ring.rb +26 -26
  23. data/lib/redis/pipeline.rb +43 -222
  24. data/lib/redis/subscribe.rb +15 -9
  25. data/lib/redis/version.rb +1 -1
  26. data/lib/redis.rb +67 -179
  27. metadata +13 -57
  28. data/lib/redis/cluster/command.rb +0 -79
  29. data/lib/redis/cluster/command_loader.rb +0 -33
  30. data/lib/redis/cluster/key_slot_converter.rb +0 -72
  31. data/lib/redis/cluster/node.rb +0 -120
  32. data/lib/redis/cluster/node_key.rb +0 -31
  33. data/lib/redis/cluster/node_loader.rb +0 -34
  34. data/lib/redis/cluster/option.rb +0 -100
  35. data/lib/redis/cluster/slot.rb +0 -86
  36. data/lib/redis/cluster/slot_loader.rb +0 -46
  37. data/lib/redis/cluster.rb +0 -315
  38. data/lib/redis/connection/command_helper.rb +0 -41
  39. data/lib/redis/connection/hiredis.rb +0 -66
  40. data/lib/redis/connection/registry.rb +0 -13
  41. data/lib/redis/connection/ruby.rb +0 -437
  42. data/lib/redis/connection/synchrony.rb +0 -148
  43. data/lib/redis/connection.rb +0 -11
data/lib/redis/client.rb CHANGED
@@ -1,667 +1,130 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "socket"
4
- require "cgi"
5
- require "redis/errors"
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
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,
33
18
  }.freeze
34
19
 
35
- attr_reader :options, :connection, :command_map
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
-
91
- def initialize(options = {})
92
- @options = _parse_options(options)
93
- @reconnect = true
94
- @logger = @options[:logger]
95
- @connection = nil
96
- @command_map = {}
97
-
98
- @pending_reads = 0
99
-
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
108
- end
109
-
110
- def connect
111
- @pid = Process.pid
112
-
113
- # Don't try to reconnect when the connection is fresh
114
- with_reconnect(false) do
115
- establish_connection
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]
144
- call [:select, db] if db != 0
145
- call [:client, :setname, @options[:id]] if @options[:id]
146
- @connector.check(self)
20
+ class << self
21
+ def config(**kwargs)
22
+ super(protocol: 2, **kwargs)
147
23
  end
148
24
 
149
- self
150
- end
151
-
152
- def id
153
- @options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
154
- end
155
-
156
- def location
157
- path || "#{host}:#{port}"
158
- end
159
-
160
- def call(command)
161
- reply = process([command]) { read }
162
- raise reply if reply.is_a?(CommandError)
163
-
164
- if block_given? && reply != 'QUEUED'
165
- yield reply
166
- else
167
- reply
25
+ def sentinel(**kwargs)
26
+ super(protocol: 2, **kwargs)
168
27
  end
169
28
  end
170
29
 
171
- def call_loop(command, timeout = 0)
172
- error = nil
173
-
174
- result = with_socket_timeout(timeout) do
175
- process([command]) do
176
- loop do
177
- reply = read
178
- if reply.is_a?(CommandError)
179
- error = reply
180
- break
181
- else
182
- yield reply
183
- end
184
- end
185
- end
186
- end
187
-
188
- # Raise error when previous block broke out of the loop.
189
- raise error if error
190
-
191
- # Result is set to the value that the provided block used to break.
192
- result
30
+ def initialize(*)
31
+ super
32
+ @inherit_socket = false
33
+ @pid = Process.pid
193
34
  end
35
+ ruby2_keywords :initialize if respond_to?(:ruby2_keywords, true)
194
36
 
195
- def call_pipeline(pipeline)
196
- return [] if pipeline.futures.empty?
197
-
198
- with_reconnect pipeline.with_reconnect? do
199
- begin
200
- pipeline.finish(call_pipelined(pipeline)).tap do
201
- self.db = pipeline.db if pipeline.db
202
- end
203
- rescue ConnectionError => e
204
- return nil if pipeline.shutdown?
205
-
206
- # Assume the pipeline was sent in one piece, but execution of
207
- # SHUTDOWN caused none of the replies for commands that were executed
208
- # prior to it from coming back around.
209
- raise e
210
- end
211
- end
37
+ def id
38
+ config.id
212
39
  end
213
40
 
214
- def call_pipelined(pipeline)
215
- return [] if pipeline.futures.empty?
216
-
217
- # The method #ensure_connected (called from #process) reconnects once on
218
- # I/O errors. To make an effort in making sure that commands are not
219
- # executed more than once, only allow reconnection before the first reply
220
- # has been read. When an error occurs after the first reply has been
221
- # read, retrying would re-execute the entire pipeline, thus re-issuing
222
- # already successfully executed commands. To circumvent this, don't retry
223
- # after the first reply has been read successfully.
224
-
225
- commands = pipeline.commands
226
-
227
- result = Array.new(commands.size)
228
- reconnect = @reconnect
229
-
230
- begin
231
- exception = nil
232
-
233
- process(commands) do
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
242
- exception = reply if exception.nil? && reply.is_a?(CommandError)
243
- end
244
- end
245
-
246
- raise exception if exception
247
- ensure
248
- @reconnect = reconnect
249
- end
250
-
251
- result
41
+ def server_url
42
+ config.server_url
252
43
  end
253
44
 
254
- def call_with_timeout(command, extra_timeout, &blk)
255
- timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
256
- with_socket_timeout(timeout) do
257
- call(command, &blk)
258
- end
259
- rescue ConnectionError
260
- retry
45
+ def timeout
46
+ config.read_timeout
261
47
  end
262
48
 
263
- def call_without_timeout(command, &blk)
264
- call_with_timeout(command, 0, &blk)
49
+ def db
50
+ config.db
265
51
  end
266
52
 
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
53
+ def host
54
+ config.host unless config.path
282
55
  end
283
56
 
284
- def connected?
285
- !!(connection && connection.connected?)
57
+ def port
58
+ config.port unless config.path
286
59
  end
287
60
 
288
- def disconnect
289
- connection.disconnect if connected?
61
+ def path
62
+ config.path
290
63
  end
291
- alias close disconnect
292
64
 
293
- def reconnect
294
- disconnect
295
- connect
65
+ def username
66
+ config.username
296
67
  end
297
68
 
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, EOFError => e
306
- raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
69
+ def password
70
+ config.password
307
71
  end
308
72
 
309
- def read
310
- io do
311
- value = connection.read
312
- @pending_reads -= 1
313
- value
314
- end
315
- end
73
+ undef_method :call
74
+ undef_method :call_once
75
+ undef_method :call_once_v
76
+ undef_method :blocking_call
316
77
 
317
- def write(command)
318
- io do
319
- @pending_reads += 1
320
- connection.write(command)
321
- end
78
+ def call_v(command, &block)
79
+ super(command, &block)
80
+ rescue ::RedisClient::Error => error
81
+ raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace
322
82
  end
323
83
 
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
84
+ def blocking_call_v(timeout, command, &block)
85
+ if timeout && timeout > 0
86
+ # Can't use the command timeout argument as the connection timeout
87
+ # otherwise it would be very racy. So we add an extra 100ms to account for
88
+ # the network delay.
89
+ timeout += 0.1
335
90
  end
336
- end
337
-
338
- def without_socket_timeout(&blk)
339
- with_socket_timeout(0, &blk)
340
- end
341
91
 
342
- def with_reconnect(val = true)
343
- original, @reconnect = @reconnect, val
344
- yield
345
- ensure
346
- @reconnect = original
92
+ super(timeout, command, &block)
93
+ rescue ::RedisClient::Error => error
94
+ raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace
347
95
  end
348
96
 
349
- def without_reconnect(&blk)
350
- with_reconnect(false, &blk)
97
+ def pipelined
98
+ super
99
+ rescue ::RedisClient::Error => error
100
+ raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace
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
377
- end
378
-
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})"
103
+ def multi
104
+ super
105
+ rescue ::RedisClient::Error => error
106
+ raise ERROR_MAPPING.fetch(error.class), error.message, error.backtrace
399
107
  end
400
108
 
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
423
-
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
437
- end
438
-
439
- def _parse_options(options)
440
- return options if options[:_parsed]
441
-
442
- defaults = DEFAULTS.dup
443
- options = options.dup
444
-
445
- defaults.each_key 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
- case uri.scheme
463
- when "unix"
464
- defaults[:path] = uri.path
465
- when "redis", "rediss"
466
- defaults[:scheme] = uri.scheme
467
- defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
468
- defaults[:port] = uri.port if uri.port
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?
471
- defaults[:db] = uri.path[1..-1].to_i if uri.path
472
- defaults[:role] = :master
473
- else
474
- raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
475
- end
476
-
477
- defaults[:ssl] = true if uri.scheme == "rediss"
478
- end
479
-
480
- # Use default when option is not specified or nil
481
- defaults.each_key do |key|
482
- options[key] = defaults[key] if options[key].nil?
483
- end
484
-
485
- if options[:path]
486
- # Unix socket
487
- options[:scheme] = "unix"
488
- options.delete(:host)
489
- options.delete(:port)
490
- else
491
- # TCP socket
492
- options[:host] = options[:host].to_s
493
- options[:port] = options[:port].to_i
494
- end
495
-
496
- if options.key?(:timeout)
497
- options[:connect_timeout] ||= options[:timeout]
498
- options[:read_timeout] ||= options[:timeout]
499
- options[:write_timeout] ||= options[:timeout]
500
- end
501
-
502
- options[:connect_timeout] = Float(options[:connect_timeout])
503
- options[:read_timeout] = Float(options[:read_timeout])
504
- options[:write_timeout] = Float(options[:write_timeout])
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
-
510
- options[:db] = options[:db].to_i
511
- options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
512
-
513
- case options[:tcp_keepalive]
514
- when Hash
515
- %i[time intvl probes].each do |key|
516
- unless options[:tcp_keepalive][key].is_a?(Integer)
517
- raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
518
- end
519
- end
520
-
521
- when Integer
522
- if options[:tcp_keepalive] >= 60
523
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
524
-
525
- elsif options[:tcp_keepalive] >= 30
526
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
527
-
528
- elsif options[:tcp_keepalive] >= 5
529
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
530
- end
531
- end
532
-
533
- options[:_parsed] = true
534
-
535
- options
109
+ def disable_reconnection(&block)
110
+ ensure_connected(retryable: false, &block)
536
111
  end
537
112
 
538
- def _parse_driver(driver)
539
- driver = driver.to_s if driver.is_a?(Symbol)
540
-
541
- if driver.is_a?(String)
542
- begin
543
- require_relative "connection/#{driver}"
544
- rescue LoadError, NameError
545
- begin
546
- require "redis/connection/#{driver}"
547
- rescue LoadError, NameError => error
548
- raise "Cannot load driver #{driver.inspect}: #{error.message}"
549
- end
550
- end
551
-
552
- driver = Connection.const_get(driver.capitalize)
553
- end
554
-
555
- driver
113
+ def inherit_socket!
114
+ @inherit_socket = true
556
115
  end
557
116
 
558
- class Connector
559
- def initialize(options)
560
- @options = options.dup
561
- end
117
+ private
562
118
 
563
- def resolve
564
- @options
119
+ def ensure_connected(retryable: true)
120
+ unless @inherit_socket || Process.pid == @pid
121
+ raise InheritedError,
122
+ "Tried to use a connection from a child process without reconnecting. " \
123
+ "You need to reconnect to Redis after forking " \
124
+ "or set :inherit_socket to true."
565
125
  end
566
126
 
567
- def check(client); end
568
-
569
- class Sentinel < Connector
570
- def initialize(options)
571
- super(options)
572
-
573
- @options[:db] = DEFAULTS.fetch(:db)
574
-
575
- @sentinels = @options.delete(:sentinels).dup
576
- @role = (@options[:role] || "master").to_s
577
- @master = @options[:host]
578
- end
579
-
580
- def check(client)
581
- # Check the instance is really of the role we are looking for.
582
- # We can't assume the command is supported since it was introduced
583
- # recently and this client should work with old stuff.
584
- begin
585
- role = client.call([:role])[0]
586
- rescue Redis::CommandError
587
- # Assume the test is passed if we can't get a reply from ROLE...
588
- role = @role
589
- end
590
-
591
- if role != @role
592
- client.disconnect
593
- raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
594
- end
595
- end
596
-
597
- def resolve
598
- result = case @role
599
- when "master"
600
- resolve_master
601
- when "slave"
602
- resolve_slave
603
- else
604
- raise ArgumentError, "Unknown instance role #{@role}"
605
- end
606
-
607
- result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
608
- end
609
-
610
- def sentinel_detect
611
- @sentinels.each do |sentinel|
612
- client = Client.new(@options.merge({
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
- }))
619
-
620
- begin
621
- if result = yield(client)
622
- # This sentinel responded. Make sure we ask it first next time.
623
- @sentinels.delete(sentinel)
624
- @sentinels.unshift(sentinel)
625
-
626
- return result
627
- end
628
- rescue BaseConnectionError
629
- ensure
630
- client.disconnect
631
- end
632
- end
633
-
634
- raise CannotConnectError, "No sentinels available."
635
- end
636
-
637
- def resolve_master
638
- sentinel_detect do |client|
639
- if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
640
- { host: reply[0], port: reply[1] }
641
- end
642
- end
643
- end
644
-
645
- def resolve_slave
646
- sentinel_detect do |client|
647
- if reply = client.call(["sentinel", "slaves", @master])
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
661
- end
662
- end
663
- end
664
- end
127
+ super
665
128
  end
666
129
  end
667
130
  end