redis 4.8.1 → 5.4.0

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +82 -0
  3. data/README.md +125 -162
  4. data/lib/redis/client.rb +82 -616
  5. data/lib/redis/commands/bitmaps.rb +14 -4
  6. data/lib/redis/commands/cluster.rb +1 -18
  7. data/lib/redis/commands/connection.rb +5 -10
  8. data/lib/redis/commands/geo.rb +3 -3
  9. data/lib/redis/commands/hashes.rb +13 -6
  10. data/lib/redis/commands/hyper_log_log.rb +1 -1
  11. data/lib/redis/commands/keys.rb +27 -23
  12. data/lib/redis/commands/lists.rb +74 -25
  13. data/lib/redis/commands/pubsub.rb +34 -25
  14. data/lib/redis/commands/server.rb +15 -15
  15. data/lib/redis/commands/sets.rb +35 -40
  16. data/lib/redis/commands/sorted_sets.rb +128 -18
  17. data/lib/redis/commands/streams.rb +48 -21
  18. data/lib/redis/commands/strings.rb +18 -17
  19. data/lib/redis/commands/transactions.rb +7 -31
  20. data/lib/redis/commands.rb +11 -12
  21. data/lib/redis/distributed.rb +136 -72
  22. data/lib/redis/errors.rb +15 -50
  23. data/lib/redis/hash_ring.rb +26 -26
  24. data/lib/redis/pipeline.rb +47 -222
  25. data/lib/redis/subscribe.rb +50 -14
  26. data/lib/redis/version.rb +1 -1
  27. data/lib/redis.rb +77 -184
  28. metadata +10 -57
  29. data/lib/redis/cluster/command.rb +0 -79
  30. data/lib/redis/cluster/command_loader.rb +0 -33
  31. data/lib/redis/cluster/key_slot_converter.rb +0 -72
  32. data/lib/redis/cluster/node.rb +0 -120
  33. data/lib/redis/cluster/node_key.rb +0 -31
  34. data/lib/redis/cluster/node_loader.rb +0 -34
  35. data/lib/redis/cluster/option.rb +0 -100
  36. data/lib/redis/cluster/slot.rb +0 -86
  37. data/lib/redis/cluster/slot_loader.rb +0 -46
  38. data/lib/redis/cluster.rb +0 -315
  39. data/lib/redis/connection/command_helper.rb +0 -41
  40. data/lib/redis/connection/hiredis.rb +0 -68
  41. data/lib/redis/connection/registry.rb +0 -13
  42. data/lib/redis/connection/ruby.rb +0 -437
  43. data/lib/redis/connection/synchrony.rb +0 -148
  44. data/lib/redis/connection.rb +0 -11
data/lib/redis/client.rb CHANGED
@@ -1,658 +1,124 @@
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
33
- }.freeze
34
-
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)
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
+ }
20
+
21
+ class << self
22
+ def config(**kwargs)
23
+ super(protocol: 2, **kwargs)
24
+ end
25
+
26
+ def sentinel(**kwargs)
27
+ super(protocol: 2, **kwargs, client_implementation: ::RedisClient)
28
+ end
29
+
30
+ def translate_error!(error, mapping: ERROR_MAPPING)
31
+ redis_error = translate_error_class(error.class, mapping: mapping)
32
+ raise redis_error, error.message, error.backtrace
33
+ end
34
+
35
+ private
36
+
37
+ def translate_error_class(error_class, mapping: ERROR_MAPPING)
38
+ mapping.fetch(error_class)
39
+ rescue IndexError
40
+ if (client_error = error_class.ancestors.find { |a| mapping[a] })
41
+ mapping[error_class] = mapping[client_error]
105
42
  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
43
+ raise
141
44
  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)
147
45
  end
148
-
149
- self
150
46
  end
151
47
 
152
48
  def id
153
- @options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
49
+ config.id
154
50
  end
155
51
 
156
- def location
157
- path || "#{host}:#{port}"
52
+ def server_url
53
+ config.server_url
158
54
  end
159
55
 
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
168
- end
169
- end
170
-
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
193
- end
194
-
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
212
- end
213
-
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
252
- end
253
-
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
261
- end
262
-
263
- def call_without_timeout(command, &blk)
264
- call_with_timeout(command, 0, &blk)
265
- end
266
-
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
282
- end
283
-
284
- def connected?
285
- !!(connection && connection.connected?)
286
- end
287
-
288
- def disconnect
289
- connection.disconnect if connected?
56
+ def timeout
57
+ config.read_timeout
290
58
  end
291
- alias close disconnect
292
59
 
293
- def reconnect
294
- disconnect
295
- connect
60
+ def db
61
+ config.db
296
62
  end
297
63
 
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]
64
+ def host
65
+ config.host unless config.path
307
66
  end
308
67
 
309
- def read
310
- io do
311
- value = connection.read
312
- @pending_reads -= 1
313
- value
314
- end
68
+ def port
69
+ config.port unless config.path
315
70
  end
316
71
 
317
- def write(command)
318
- io do
319
- @pending_reads += 1
320
- connection.write(command)
321
- end
72
+ def path
73
+ config.path
322
74
  end
323
75
 
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
335
- end
76
+ def username
77
+ config.username
336
78
  end
337
79
 
338
- def without_socket_timeout(&blk)
339
- with_socket_timeout(0, &blk)
80
+ def password
81
+ config.password
340
82
  end
341
83
 
342
- def with_reconnect(val = true)
343
- original, @reconnect = @reconnect, val
344
- yield
345
- ensure
346
- @reconnect = original
347
- end
84
+ undef_method :call
85
+ undef_method :call_once
86
+ undef_method :call_once_v
87
+ undef_method :blocking_call
348
88
 
349
- def without_reconnect(&blk)
350
- with_reconnect(false, &blk)
89
+ def call_v(command, &block)
90
+ super(command, &block)
91
+ rescue ::RedisClient::Error => error
92
+ Client.translate_error!(error)
351
93
  end
352
94
 
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
95
+ def blocking_call_v(timeout, command, &block)
96
+ if timeout && timeout > 0
97
+ # Can't use the command timeout argument as the connection timeout
98
+ # otherwise it would be very racy. So we add the regular read_timeout on top
99
+ # to account for the network delay.
100
+ timeout += config.read_timeout
376
101
  end
377
- end
378
-
379
- def establish_connection
380
- server = @connector.resolve.dup
381
102
 
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
+ super(timeout, command, &block)
104
+ rescue ::RedisClient::Error => error
105
+ Client.translate_error!(error)
399
106
  end
400
107
 
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
108
+ def pipelined(exception: true)
109
+ super
110
+ rescue ::RedisClient::Error => error
111
+ Client.translate_error!(error)
428
112
  end
429
113
 
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
114
+ def multi(watch: nil)
115
+ super
116
+ rescue ::RedisClient::Error => error
117
+ Client.translate_error!(error)
527
118
  end
528
119
 
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
547
- end
548
-
549
- class Connector
550
- def initialize(options)
551
- @options = options.dup
552
- end
553
-
554
- def resolve
555
- @options
556
- end
557
-
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
120
+ def inherit_socket!
121
+ @inherit_socket = true
656
122
  end
657
123
  end
658
124
  end