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