redis 4.5.1 → 5.0.6

Sign up to get free protection for your applications and to get access to all the features.
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