redis 3.0.0 → 4.5.0

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