redis 3.3.5 → 5.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (137) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +290 -2
  3. data/README.md +146 -146
  4. data/lib/redis/client.rb +79 -541
  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 +339 -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 +884 -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 +328 -108
  23. data/lib/redis/errors.rb +23 -1
  24. data/lib/redis/hash_ring.rb +36 -79
  25. data/lib/redis/pipeline.rb +69 -83
  26. data/lib/redis/subscribe.rb +26 -19
  27. data/lib/redis/version.rb +3 -1
  28. data/lib/redis.rb +115 -2695
  29. metadata +38 -218
  30. data/.gitignore +0 -16
  31. data/.travis/Gemfile +0 -11
  32. data/.travis.yml +0 -89
  33. data/.yardopts +0 -3
  34. data/Gemfile +0 -4
  35. data/Rakefile +0 -87
  36. data/benchmarking/logging.rb +0 -71
  37. data/benchmarking/pipeline.rb +0 -51
  38. data/benchmarking/speed.rb +0 -21
  39. data/benchmarking/suite.rb +0 -24
  40. data/benchmarking/worker.rb +0 -71
  41. data/examples/basic.rb +0 -15
  42. data/examples/consistency.rb +0 -114
  43. data/examples/dist_redis.rb +0 -43
  44. data/examples/incr-decr.rb +0 -17
  45. data/examples/list.rb +0 -26
  46. data/examples/pubsub.rb +0 -37
  47. data/examples/sentinel/sentinel.conf +0 -9
  48. data/examples/sentinel/start +0 -49
  49. data/examples/sentinel.rb +0 -41
  50. data/examples/sets.rb +0 -36
  51. data/examples/unicorn/config.ru +0 -3
  52. data/examples/unicorn/unicorn.rb +0 -20
  53. data/lib/redis/connection/command_helper.rb +0 -44
  54. data/lib/redis/connection/hiredis.rb +0 -66
  55. data/lib/redis/connection/registry.rb +0 -12
  56. data/lib/redis/connection/ruby.rb +0 -429
  57. data/lib/redis/connection/synchrony.rb +0 -133
  58. data/lib/redis/connection.rb +0 -9
  59. data/redis.gemspec +0 -44
  60. data/test/bitpos_test.rb +0 -69
  61. data/test/blocking_commands_test.rb +0 -42
  62. data/test/client_test.rb +0 -59
  63. data/test/command_map_test.rb +0 -30
  64. data/test/commands_on_hashes_test.rb +0 -21
  65. data/test/commands_on_hyper_log_log_test.rb +0 -21
  66. data/test/commands_on_lists_test.rb +0 -20
  67. data/test/commands_on_sets_test.rb +0 -77
  68. data/test/commands_on_sorted_sets_test.rb +0 -137
  69. data/test/commands_on_strings_test.rb +0 -101
  70. data/test/commands_on_value_types_test.rb +0 -133
  71. data/test/connection_handling_test.rb +0 -277
  72. data/test/connection_test.rb +0 -57
  73. data/test/db/.gitkeep +0 -0
  74. data/test/distributed_blocking_commands_test.rb +0 -46
  75. data/test/distributed_commands_on_hashes_test.rb +0 -10
  76. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  77. data/test/distributed_commands_on_lists_test.rb +0 -22
  78. data/test/distributed_commands_on_sets_test.rb +0 -83
  79. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  80. data/test/distributed_commands_on_strings_test.rb +0 -59
  81. data/test/distributed_commands_on_value_types_test.rb +0 -95
  82. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  83. data/test/distributed_connection_handling_test.rb +0 -23
  84. data/test/distributed_internals_test.rb +0 -79
  85. data/test/distributed_key_tags_test.rb +0 -52
  86. data/test/distributed_persistence_control_commands_test.rb +0 -26
  87. data/test/distributed_publish_subscribe_test.rb +0 -92
  88. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  89. data/test/distributed_scripting_test.rb +0 -102
  90. data/test/distributed_sorting_test.rb +0 -20
  91. data/test/distributed_test.rb +0 -58
  92. data/test/distributed_transactions_test.rb +0 -32
  93. data/test/encoding_test.rb +0 -18
  94. data/test/error_replies_test.rb +0 -59
  95. data/test/fork_safety_test.rb +0 -65
  96. data/test/helper.rb +0 -232
  97. data/test/helper_test.rb +0 -24
  98. data/test/internals_test.rb +0 -417
  99. data/test/lint/blocking_commands.rb +0 -150
  100. data/test/lint/hashes.rb +0 -162
  101. data/test/lint/hyper_log_log.rb +0 -60
  102. data/test/lint/lists.rb +0 -143
  103. data/test/lint/sets.rb +0 -140
  104. data/test/lint/sorted_sets.rb +0 -316
  105. data/test/lint/strings.rb +0 -260
  106. data/test/lint/value_types.rb +0 -122
  107. data/test/persistence_control_commands_test.rb +0 -26
  108. data/test/pipelining_commands_test.rb +0 -242
  109. data/test/publish_subscribe_test.rb +0 -282
  110. data/test/remote_server_control_commands_test.rb +0 -118
  111. data/test/scanning_test.rb +0 -413
  112. data/test/scripting_test.rb +0 -78
  113. data/test/sentinel_command_test.rb +0 -80
  114. data/test/sentinel_test.rb +0 -255
  115. data/test/sorting_test.rb +0 -59
  116. data/test/ssl_test.rb +0 -73
  117. data/test/support/connection/hiredis.rb +0 -1
  118. data/test/support/connection/ruby.rb +0 -1
  119. data/test/support/connection/synchrony.rb +0 -17
  120. data/test/support/redis_mock.rb +0 -130
  121. data/test/support/ssl/gen_certs.sh +0 -31
  122. data/test/support/ssl/trusted-ca.crt +0 -25
  123. data/test/support/ssl/trusted-ca.key +0 -27
  124. data/test/support/ssl/trusted-cert.crt +0 -81
  125. data/test/support/ssl/trusted-cert.key +0 -28
  126. data/test/support/ssl/untrusted-ca.crt +0 -26
  127. data/test/support/ssl/untrusted-ca.key +0 -27
  128. data/test/support/ssl/untrusted-cert.crt +0 -82
  129. data/test/support/ssl/untrusted-cert.key +0 -28
  130. data/test/support/wire/synchrony.rb +0 -24
  131. data/test/support/wire/thread.rb +0 -5
  132. data/test/synchrony_driver.rb +0 -88
  133. data/test/test.conf.erb +0 -9
  134. data/test/thread_safety_test.rb +0 -62
  135. data/test/transactions_test.rb +0 -264
  136. data/test/unknown_commands_test.rb +0 -14
  137. data/test/url_param_test.rb +0 -138
data/lib/redis/client.rb CHANGED
@@ -1,590 +1,128 @@
1
- require "redis/errors"
2
- require "socket"
3
- require "cgi"
1
+ # frozen_string_literal: true
4
2
 
5
- class Redis
6
- class Client
3
+ require 'redis-client'
7
4
 
8
- DEFAULTS = {
9
- :url => lambda { ENV["REDIS_URL"] },
10
- :scheme => "redis",
11
- :host => "127.0.0.1",
12
- :port => 6379,
13
- :path => nil,
14
- :timeout => 5.0,
15
- :password => nil,
16
- :db => 0,
17
- :driver => nil,
18
- :id => nil,
19
- :tcp_keepalive => 0,
20
- :reconnect_attempts => 1,
21
- :inherit_socket => false
5
+ class Redis
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,
22
19
  }
23
20
 
24
- def options
25
- Marshal.load(Marshal.dump(@options))
26
- end
27
-
28
- def scheme
29
- @options[:scheme]
30
- end
31
-
32
- def host
33
- @options[:host]
34
- end
35
-
36
- def port
37
- @options[:port]
38
- end
39
-
40
- def path
41
- @options[:path]
42
- end
43
-
44
- def read_timeout
45
- @options[:read_timeout]
46
- end
47
-
48
- def connect_timeout
49
- @options[:connect_timeout]
50
- end
51
-
52
- def timeout
53
- @options[:read_timeout]
54
- end
55
-
56
- def password
57
- @options[:password]
58
- end
59
-
60
- def db
61
- @options[:db]
62
- end
63
-
64
- def db=(db)
65
- @options[:db] = db.to_i
66
- end
67
-
68
- def driver
69
- @options[:driver]
70
- end
71
-
72
- def inherit_socket?
73
- @options[:inherit_socket]
74
- end
75
-
76
- attr_accessor :logger
77
- attr_reader :connection
78
- attr_reader :command_map
79
-
80
- def initialize(options = {})
81
- @options = _parse_options(options)
82
- @reconnect = true
83
- @logger = @options[:logger]
84
- @connection = nil
85
- @command_map = {}
86
-
87
- @pending_reads = 0
88
-
89
- if options.include?(:sentinels)
90
- @connector = Connector::Sentinel.new(@options)
91
- else
92
- @connector = Connector.new(@options)
21
+ class << self
22
+ def config(**kwargs)
23
+ super(protocol: 2, **kwargs)
93
24
  end
94
- end
95
25
 
96
- def connect
97
- @pid = Process.pid
98
-
99
- # Don't try to reconnect when the connection is fresh
100
- with_reconnect(false) do
101
- establish_connection
102
- call [:auth, password] if password
103
- call [:select, db] if db != 0
104
- call [:client, :setname, @options[:id]] if @options[:id]
105
- @connector.check(self)
26
+ def sentinel(**kwargs)
27
+ super(protocol: 2, **kwargs, client_implementation: ::RedisClient)
106
28
  end
107
29
 
108
- self
109
- end
110
-
111
- def id
112
- @options[:id] || "redis://#{location}/#{db}"
113
- end
114
-
115
- def location
116
- path || "#{host}:#{port}"
117
- end
118
-
119
- def call(command)
120
- reply = process([command]) { read }
121
- raise reply if reply.is_a?(CommandError)
122
-
123
- if block_given?
124
- yield reply
125
- else
126
- reply
30
+ def translate_error!(error)
31
+ redis_error = translate_error_class(error.class)
32
+ raise redis_error, error.message, error.backtrace
127
33
  end
128
- end
129
-
130
- def call_loop(command, timeout = 0)
131
- error = nil
132
-
133
- result = with_socket_timeout(timeout) do
134
- process([command]) do
135
- loop do
136
- reply = read
137
- if reply.is_a?(CommandError)
138
- error = reply
139
- break
140
- else
141
- yield reply
142
- end
143
- end
144
- end
145
- end
146
-
147
- # Raise error when previous block broke out of the loop.
148
- raise error if error
149
-
150
- # Result is set to the value that the provided block used to break.
151
- result
152
- end
153
-
154
- def call_pipeline(pipeline)
155
- with_reconnect pipeline.with_reconnect? do
156
- begin
157
- pipeline.finish(call_pipelined(pipeline.commands)).tap do
158
- self.db = pipeline.db if pipeline.db
159
- end
160
- rescue ConnectionError => e
161
- return nil if pipeline.shutdown?
162
- # Assume the pipeline was sent in one piece, but execution of
163
- # SHUTDOWN caused none of the replies for commands that were executed
164
- # prior to it from coming back around.
165
- raise e
166
- end
167
- end
168
- end
169
-
170
- def call_pipelined(commands)
171
- return [] if commands.empty?
172
-
173
- # The method #ensure_connected (called from #process) reconnects once on
174
- # I/O errors. To make an effort in making sure that commands are not
175
- # executed more than once, only allow reconnection before the first reply
176
- # has been read. When an error occurs after the first reply has been
177
- # read, retrying would re-execute the entire pipeline, thus re-issuing
178
- # already successfully executed commands. To circumvent this, don't retry
179
- # after the first reply has been read successfully.
180
-
181
- result = Array.new(commands.size)
182
- reconnect = @reconnect
183
-
184
- begin
185
- exception = nil
186
-
187
- process(commands) do
188
- result[0] = read
189
34
 
190
- @reconnect = false
35
+ private
191
36
 
192
- (commands.size - 1).times do |i|
193
- reply = read
194
- result[i + 1] = reply
195
- exception = reply if exception.nil? && reply.is_a?(CommandError)
196
- end
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]
42
+ else
43
+ raise
197
44
  end
198
-
199
- raise exception if exception
200
- ensure
201
- @reconnect = reconnect
202
45
  end
203
-
204
- result
205
46
  end
206
47
 
207
- def call_with_timeout(command, timeout, &blk)
208
- with_socket_timeout(timeout) do
209
- call(command, &blk)
210
- end
211
- rescue ConnectionError
212
- retry
48
+ def id
49
+ config.id
213
50
  end
214
51
 
215
- def call_without_timeout(command, &blk)
216
- call_with_timeout(command, 0, &blk)
52
+ def server_url
53
+ config.server_url
217
54
  end
218
55
 
219
- def process(commands)
220
- logging(commands) do
221
- ensure_connected do
222
- commands.each do |command|
223
- if command_map[command.first]
224
- command = command.dup
225
- command[0] = command_map[command.first]
226
- end
227
-
228
- write(command)
229
- end
230
-
231
- yield if block_given?
232
- end
233
- end
234
- end
235
-
236
- def connected?
237
- !! (connection && connection.connected?)
56
+ def timeout
57
+ config.read_timeout
238
58
  end
239
59
 
240
- def disconnect
241
- connection.disconnect if connected?
60
+ def db
61
+ config.db
242
62
  end
243
63
 
244
- def reconnect
245
- disconnect
246
- connect
64
+ def host
65
+ config.host unless config.path
247
66
  end
248
67
 
249
- def io
250
- yield
251
- rescue TimeoutError => e1
252
- # Add a message to the exception without destroying the original stack
253
- e2 = TimeoutError.new("Connection timed out")
254
- e2.set_backtrace(e1.backtrace)
255
- raise e2
256
- rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
257
- raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
68
+ def port
69
+ config.port unless config.path
258
70
  end
259
71
 
260
- def read
261
- io do
262
- value = connection.read
263
- @pending_reads -= 1
264
- value
265
- end
72
+ def path
73
+ config.path
266
74
  end
267
75
 
268
- def write(command)
269
- io do
270
- @pending_reads += 1
271
- connection.write(command)
272
- end
76
+ def username
77
+ config.username
273
78
  end
274
79
 
275
- def with_socket_timeout(timeout)
276
- connect unless connected?
277
-
278
- begin
279
- connection.timeout = timeout
280
- yield
281
- ensure
282
- connection.timeout = self.timeout if connected?
283
- end
284
- end
285
-
286
- def without_socket_timeout(&blk)
287
- with_socket_timeout(0, &blk)
80
+ def password
81
+ config.password
288
82
  end
289
83
 
290
- def with_reconnect(val=true)
291
- begin
292
- original, @reconnect = @reconnect, val
293
- yield
294
- ensure
295
- @reconnect = original
296
- end
297
- end
84
+ undef_method :call
85
+ undef_method :call_once
86
+ undef_method :call_once_v
87
+ undef_method :blocking_call
298
88
 
299
- def without_reconnect(&blk)
300
- with_reconnect(false, &blk)
89
+ def call_v(command, &block)
90
+ super(command, &block)
91
+ rescue ::RedisClient::Error => error
92
+ Client.translate_error!(error)
301
93
  end
302
94
 
303
- protected
304
-
305
- def logging(commands)
306
- return yield unless @logger && @logger.debug?
307
-
308
- begin
309
- commands.each do |name, *args|
310
- logged_args = args.map do |a|
311
- case
312
- when a.respond_to?(:inspect) then a.inspect
313
- when a.respond_to?(:to_s) then a.to_s
314
- else
315
- # handle poorly-behaved descendants of BasicObject
316
- klass = a.instance_exec { (class << self; self end).superclass }
317
- "\#<#{klass}:#{a.__id__}>"
318
- end
319
- end
320
- @logger.debug("[Redis] command=#{name.to_s.upcase} args=#{logged_args.join(' ')}")
321
- end
322
-
323
- t1 = Time.now
324
- yield
325
- ensure
326
- @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
327
101
  end
328
- end
329
-
330
- def establish_connection
331
- server = @connector.resolve.dup
332
102
 
333
- @options[:host] = server[:host]
334
- @options[:port] = Integer(server[:port]) if server.include?(:port)
335
-
336
- @connection = @options[:driver].connect(@options)
337
- @pending_reads = 0
338
- rescue TimeoutError,
339
- Errno::ECONNREFUSED,
340
- Errno::EHOSTDOWN,
341
- Errno::EHOSTUNREACH,
342
- Errno::ENETUNREACH,
343
- Errno::ETIMEDOUT
344
-
345
- raise CannotConnectError, "Error connecting to Redis on #{location} (#{$!.class})"
103
+ super(timeout, command, &block)
104
+ rescue ::RedisClient::Error => error
105
+ Client.translate_error!(error)
346
106
  end
347
107
 
348
- def ensure_connected
349
- disconnect if @pending_reads > 0
350
-
351
- attempts = 0
352
-
353
- begin
354
- attempts += 1
355
-
356
- if connected?
357
- unless inherit_socket? || Process.pid == @pid
358
- raise InheritedError,
359
- "Tried to use a connection from a child process without reconnecting. " +
360
- "You need to reconnect to Redis after forking " +
361
- "or set :inherit_socket to true."
362
- end
363
- else
364
- connect
365
- end
366
-
367
- yield
368
- rescue BaseConnectionError
369
- disconnect
370
-
371
- if attempts <= @options[:reconnect_attempts] && @reconnect
372
- retry
373
- else
374
- raise
375
- end
376
- rescue Exception
377
- disconnect
378
- raise
379
- end
108
+ def pipelined
109
+ super
110
+ rescue ::RedisClient::Error => error
111
+ Client.translate_error!(error)
380
112
  end
381
113
 
382
- def _parse_options(options)
383
- return options if options[:_parsed]
384
-
385
- defaults = DEFAULTS.dup
386
- options = options.dup
387
-
388
- defaults.keys.each do |key|
389
- # Fill in defaults if needed
390
- if defaults[key].respond_to?(:call)
391
- defaults[key] = defaults[key].call
392
- end
393
-
394
- # Symbolize only keys that are needed
395
- options[key] = options[key.to_s] if options.has_key?(key.to_s)
396
- end
397
-
398
- url = options[:url] || defaults[:url]
399
-
400
- # Override defaults from URL if given
401
- if url
402
- require "uri"
403
-
404
- uri = URI(url)
405
-
406
- if uri.scheme == "unix"
407
- defaults[:path] = uri.path
408
- elsif uri.scheme == "redis" || uri.scheme == "rediss"
409
- defaults[:scheme] = uri.scheme
410
- defaults[:host] = uri.host if uri.host
411
- defaults[:port] = uri.port if uri.port
412
- defaults[:password] = CGI.unescape(uri.password) if uri.password
413
- defaults[:db] = uri.path[1..-1].to_i if uri.path
414
- defaults[:role] = :master
415
- else
416
- raise ArgumentError, "invalid uri scheme '#{uri.scheme}'"
417
- end
418
-
419
- defaults[:ssl] = true if uri.scheme == "rediss"
420
- end
421
-
422
- # Use default when option is not specified or nil
423
- defaults.keys.each do |key|
424
- options[key] = defaults[key] if options[key].nil?
425
- end
426
-
427
- if options[:path]
428
- # Unix socket
429
- options[:scheme] = "unix"
430
- options.delete(:host)
431
- options.delete(:port)
432
- else
433
- # TCP socket
434
- options[:host] = options[:host].to_s
435
- options[:port] = options[:port].to_i
436
- end
437
-
438
- if options.has_key?(:timeout)
439
- options[:connect_timeout] ||= options[:timeout]
440
- options[:read_timeout] ||= options[:timeout]
441
- options[:write_timeout] ||= options[:timeout]
442
- end
443
-
444
- options[:connect_timeout] = Float(options[:connect_timeout])
445
- options[:read_timeout] = Float(options[:read_timeout])
446
- options[:write_timeout] = Float(options[:write_timeout])
447
-
448
- options[:db] = options[:db].to_i
449
- options[:driver] = _parse_driver(options[:driver]) || Connection.drivers.last
450
-
451
- case options[:tcp_keepalive]
452
- when Hash
453
- [:time, :intvl, :probes].each do |key|
454
- unless options[:tcp_keepalive][key].is_a?(Integer)
455
- raise "Expected the #{key.inspect} key in :tcp_keepalive to be an Integer"
456
- end
457
- end
458
-
459
- when Integer
460
- if options[:tcp_keepalive] >= 60
461
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 20, :intvl => 10, :probes => 2}
462
-
463
- elsif options[:tcp_keepalive] >= 30
464
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 10, :intvl => 5, :probes => 2}
465
-
466
- elsif options[:tcp_keepalive] >= 5
467
- options[:tcp_keepalive] = {:time => options[:tcp_keepalive] - 2, :intvl => 2, :probes => 1}
468
- end
469
- end
470
-
471
- options[:_parsed] = true
472
-
473
- options
114
+ def multi
115
+ super
116
+ rescue ::RedisClient::Error => error
117
+ Client.translate_error!(error)
474
118
  end
475
119
 
476
- def _parse_driver(driver)
477
- driver = driver.to_s if driver.is_a?(Symbol)
478
-
479
- if driver.kind_of?(String)
480
- begin
481
- require "redis/connection/#{driver}"
482
- driver = Connection.const_get(driver.capitalize)
483
- rescue LoadError, NameError
484
- raise RuntimeError, "Cannot load driver #{driver.inspect}"
485
- end
486
- end
487
-
488
- driver
120
+ def disable_reconnection(&block)
121
+ ensure_connected(retryable: false, &block)
489
122
  end
490
123
 
491
- class Connector
492
- def initialize(options)
493
- @options = options.dup
494
- end
495
-
496
- def resolve
497
- @options
498
- end
499
-
500
- def check(client)
501
- end
502
-
503
- class Sentinel < Connector
504
- def initialize(options)
505
- super(options)
506
-
507
- @options[:password] = DEFAULTS.fetch(:password)
508
- @options[:db] = DEFAULTS.fetch(:db)
509
-
510
- @sentinels = @options.delete(:sentinels).dup
511
- @role = @options.fetch(:role, "master").to_s
512
- @master = @options[:host]
513
- end
514
-
515
- def check(client)
516
- # Check the instance is really of the role we are looking for.
517
- # We can't assume the command is supported since it was introduced
518
- # recently and this client should work with old stuff.
519
- begin
520
- role = client.call([:role])[0]
521
- rescue Redis::CommandError
522
- # Assume the test is passed if we can't get a reply from ROLE...
523
- role = @role
524
- end
525
-
526
- if role != @role
527
- client.disconnect
528
- raise ConnectionError, "Instance role mismatch. Expected #{@role}, got #{role}."
529
- end
530
- end
531
-
532
- def resolve
533
- result = case @role
534
- when "master"
535
- resolve_master
536
- when "slave"
537
- resolve_slave
538
- else
539
- raise ArgumentError, "Unknown instance role #{@role}"
540
- end
541
-
542
- result || (raise ConnectionError, "Unable to fetch #{@role} via Sentinel.")
543
- end
544
-
545
- def sentinel_detect
546
- @sentinels.each do |sentinel|
547
- client = Client.new(@options.merge({
548
- :host => sentinel[:host],
549
- :port => sentinel[:port],
550
- :reconnect_attempts => 0,
551
- }))
552
-
553
- begin
554
- if result = yield(client)
555
- # This sentinel responded. Make sure we ask it first next time.
556
- @sentinels.delete(sentinel)
557
- @sentinels.unshift(sentinel)
558
-
559
- return result
560
- end
561
- rescue BaseConnectionError
562
- ensure
563
- client.disconnect
564
- end
565
- end
566
-
567
- raise CannotConnectError, "No sentinels available."
568
- end
569
-
570
- def resolve_master
571
- sentinel_detect do |client|
572
- if reply = client.call(["sentinel", "get-master-addr-by-name", @master])
573
- {:host => reply[0], :port => reply[1]}
574
- end
575
- end
576
- end
577
-
578
- def resolve_slave
579
- sentinel_detect do |client|
580
- if reply = client.call(["sentinel", "slaves", @master])
581
- slave = Hash[*reply.sample]
582
-
583
- {:host => slave.fetch("ip"), :port => slave.fetch("port")}
584
- end
585
- end
586
- end
587
- end
124
+ def inherit_socket!
125
+ @inherit_socket = true
588
126
  end
589
127
  end
590
128
  end