redis 4.8.1 → 5.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +88 -168
  4. data/lib/redis/client.rb +84 -614
  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/geo.rb +3 -3
  9. data/lib/redis/commands/hashes.rb +9 -6
  10. data/lib/redis/commands/hyper_log_log.rb +1 -1
  11. data/lib/redis/commands/keys.rb +5 -23
  12. data/lib/redis/commands/lists.rb +74 -25
  13. data/lib/redis/commands/pubsub.rb +7 -25
  14. data/lib/redis/commands/server.rb +15 -15
  15. data/lib/redis/commands/sets.rb +31 -40
  16. data/lib/redis/commands/sorted_sets.rb +84 -12
  17. data/lib/redis/commands/streams.rb +39 -19
  18. data/lib/redis/commands/strings.rb +18 -17
  19. data/lib/redis/commands/transactions.rb +7 -31
  20. data/lib/redis/commands.rb +4 -7
  21. data/lib/redis/distributed.rb +114 -64
  22. data/lib/redis/errors.rb +15 -50
  23. data/lib/redis/hash_ring.rb +26 -26
  24. data/lib/redis/pipeline.rb +43 -222
  25. data/lib/redis/subscribe.rb +23 -15
  26. data/lib/redis/version.rb +1 -1
  27. data/lib/redis.rb +91 -184
  28. metadata +10 -54
  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,128 @@
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)
31
+ redis_error = translate_error_class(error.class)
32
+ raise redis_error, error.message, error.backtrace
33
+ end
34
+
35
+ private
36
+
37
+ def translate_error_class(error_class)
38
+ ERROR_MAPPING.fetch(error_class)
39
+ rescue IndexError
40
+ if (client_error = error_class.ancestors.find { |a| ERROR_MAPPING[a] })
41
+ ERROR_MAPPING[error_class] = ERROR_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}"
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
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
49
+ config.id
261
50
  end
262
51
 
263
- def call_without_timeout(command, &blk)
264
- call_with_timeout(command, 0, &blk)
52
+ def server_url
53
+ config.server_url
265
54
  end
266
55
 
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
-
382
- @options[:host] = server[:host]
383
- @options[:port] = Integer(server[:port]) if server.include?(:port)
384
102
 
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
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
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
120
+ def disable_reconnection(&block)
121
+ ensure_connected(retryable: false, &block)
547
122
  end
548
123
 
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
124
+ def inherit_socket!
125
+ @inherit_socket = true
656
126
  end
657
127
  end
658
128
  end