redis 4.8.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 +20 -10
  3. data/README.md +77 -161
  4. data/lib/redis/client.rb +78 -606
  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 +0 -18
  11. data/lib/redis/commands/lists.rb +11 -16
  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 +30 -39
  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 -6
  20. data/lib/redis/distributed.rb +85 -63
  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 +68 -181
  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 -68
  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,658 +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 || (@pid != Process.pid && !inherit_socket?)
403
-
404
- attempts = 0
405
-
406
- begin
407
- attempts += 1
408
-
409
- connect unless connected?
410
-
411
- yield
412
- rescue BaseConnectionError
413
- disconnect
414
-
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)
420
- retry
421
- else
422
- raise
423
- end
424
- rescue Exception
425
- disconnect
426
- raise
427
- end
428
- end
429
-
430
- def _parse_options(options)
431
- return options if options[:_parsed]
432
-
433
- defaults = DEFAULTS.dup
434
- options = options.dup
435
-
436
- defaults.each_key do |key|
437
- # Fill in defaults if needed
438
- defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
439
-
440
- # Symbolize only keys that are needed
441
- options[key] = options[key.to_s] if options.key?(key.to_s)
442
- end
443
-
444
- url = options[:url]
445
- url = defaults[:url] if url.nil?
446
-
447
- # Override defaults from URL if given
448
- if url
449
- require "uri"
450
-
451
- uri = URI(url)
452
-
453
- case uri.scheme
454
- when "unix"
455
- defaults[:path] = uri.path
456
- when "redis", "rediss"
457
- defaults[:scheme] = uri.scheme
458
- defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
459
- defaults[:port] = uri.port if uri.port
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?
462
- defaults[:db] = uri.path[1..-1].to_i if uri.path
463
- defaults[:role] = :master
464
- else
465
- raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
466
- end
467
-
468
- defaults[:ssl] = true if uri.scheme == "rediss"
469
- end
470
-
471
- # Use default when option is not specified or nil
472
- defaults.each_key do |key|
473
- options[key] = defaults[key] if options[key].nil?
474
- end
475
-
476
- if options[:path]
477
- # Unix socket
478
- options[:scheme] = "unix"
479
- options.delete(:host)
480
- options.delete(:port)
481
- else
482
- # TCP socket
483
- options[:host] = options[:host].to_s
484
- options[:port] = options[:port].to_i
485
- end
486
-
487
- if options.key?(:timeout)
488
- options[:connect_timeout] ||= options[:timeout]
489
- options[:read_timeout] ||= options[:timeout]
490
- options[:write_timeout] ||= options[:timeout]
491
- end
492
-
493
- options[:connect_timeout] = Float(options[:connect_timeout])
494
- options[:read_timeout] = Float(options[:read_timeout])
495
- options[:write_timeout] = Float(options[:write_timeout])
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
-
501
- options[:db] = options[:db].to_i
502
- options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
503
-
504
- case options[:tcp_keepalive]
505
- when Hash
506
- %i[time intvl probes].each do |key|
507
- unless options[:tcp_keepalive][key].is_a?(Integer)
508
- raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
509
- end
510
- end
511
-
512
- when Integer
513
- if options[:tcp_keepalive] >= 60
514
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 20, intvl: 10, probes: 2 }
515
-
516
- elsif options[:tcp_keepalive] >= 30
517
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 10, intvl: 5, probes: 2 }
518
-
519
- elsif options[:tcp_keepalive] >= 5
520
- options[:tcp_keepalive] = { time: options[:tcp_keepalive] - 2, intvl: 2, probes: 1 }
521
- end
522
- end
523
-
524
- options[:_parsed] = true
525
-
526
- options
109
+ def disable_reconnection(&block)
110
+ ensure_connected(retryable: false, &block)
527
111
  end
528
112
 
529
- def _parse_driver(driver)
530
- driver = driver.to_s if driver.is_a?(Symbol)
531
-
532
- if driver.is_a?(String)
533
- begin
534
- require_relative "connection/#{driver}"
535
- rescue LoadError, NameError
536
- begin
537
- require "redis/connection/#{driver}"
538
- rescue LoadError, NameError => error
539
- raise "Cannot load driver #{driver.inspect}: #{error.message}"
540
- end
541
- end
542
-
543
- driver = Connection.const_get(driver.capitalize)
544
- end
545
-
546
- driver
113
+ def inherit_socket!
114
+ @inherit_socket = true
547
115
  end
548
116
 
549
- class Connector
550
- def initialize(options)
551
- @options = options.dup
552
- end
117
+ private
553
118
 
554
- def resolve
555
- @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."
556
125
  end
557
126
 
558
- def check(client); end
559
-
560
- class Sentinel < Connector
561
- def initialize(options)
562
- super(options)
563
-
564
- @options[:db] = DEFAULTS.fetch(:db)
565
-
566
- @sentinels = @options.delete(:sentinels).dup
567
- @role = (@options[:role] || "master").to_s
568
- @master = @options[:host]
569
- end
570
-
571
- def check(client)
572
- # Check the instance is really of the role we are looking for.
573
- # We can't assume the command is supported since it was introduced
574
- # recently and this client should work with old stuff.
575
- begin
576
- role = client.call([:role])[0]
577
- rescue Redis::CommandError
578
- # Assume the test is passed if we can't get a reply from ROLE...
579
- role = @role
580
- end
581
-
582
- if role != @role
583
- client.disconnect
584
- raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
585
- end
586
- end
587
-
588
- def resolve
589
- result = case @role
590
- when "master"
591
- resolve_master
592
- when "slave"
593
- resolve_slave
594
- else
595
- raise ArgumentError, "Unknown instance role #{@role}"
596
- end
597
-
598
- result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
599
- end
600
-
601
- def sentinel_detect
602
- @sentinels.each do |sentinel|
603
- client = Client.new(@options.merge({
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
- }))
610
-
611
- begin
612
- if result = yield(client)
613
- # This sentinel responded. Make sure we ask it first next time.
614
- @sentinels.delete(sentinel)
615
- @sentinels.unshift(sentinel)
616
-
617
- return result
618
- end
619
- rescue BaseConnectionError
620
- ensure
621
- client.disconnect
622
- end
623
- end
624
-
625
- raise CannotConnectError, "No sentinels available."
626
- end
627
-
628
- def resolve_master
629
- sentinel_detect do |client|
630
- if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
631
- { host: reply[0], port: reply[1] }
632
- end
633
- end
634
- end
635
-
636
- def resolve_slave
637
- sentinel_detect do |client|
638
- if reply = client.call(["sentinel", "slaves", @master])
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
652
- end
653
- end
654
- end
655
- end
127
+ super
656
128
  end
657
129
  end
658
130
  end