redis 3.3.3 → 5.0.5

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 (136) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +280 -12
  3. data/README.md +141 -147
  4. data/lib/redis/client.rb +77 -539
  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 +300 -108
  23. data/lib/redis/errors.rb +22 -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 +113 -2685
  29. metadata +40 -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/db/.gitkeep +0 -0
  73. data/test/distributed_blocking_commands_test.rb +0 -46
  74. data/test/distributed_commands_on_hashes_test.rb +0 -10
  75. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  76. data/test/distributed_commands_on_lists_test.rb +0 -22
  77. data/test/distributed_commands_on_sets_test.rb +0 -83
  78. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  79. data/test/distributed_commands_on_strings_test.rb +0 -59
  80. data/test/distributed_commands_on_value_types_test.rb +0 -95
  81. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  82. data/test/distributed_connection_handling_test.rb +0 -23
  83. data/test/distributed_internals_test.rb +0 -79
  84. data/test/distributed_key_tags_test.rb +0 -52
  85. data/test/distributed_persistence_control_commands_test.rb +0 -26
  86. data/test/distributed_publish_subscribe_test.rb +0 -92
  87. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  88. data/test/distributed_scripting_test.rb +0 -102
  89. data/test/distributed_sorting_test.rb +0 -20
  90. data/test/distributed_test.rb +0 -58
  91. data/test/distributed_transactions_test.rb +0 -32
  92. data/test/encoding_test.rb +0 -18
  93. data/test/error_replies_test.rb +0 -59
  94. data/test/fork_safety_test.rb +0 -65
  95. data/test/helper.rb +0 -232
  96. data/test/helper_test.rb +0 -24
  97. data/test/internals_test.rb +0 -457
  98. data/test/lint/blocking_commands.rb +0 -150
  99. data/test/lint/hashes.rb +0 -162
  100. data/test/lint/hyper_log_log.rb +0 -60
  101. data/test/lint/lists.rb +0 -143
  102. data/test/lint/sets.rb +0 -140
  103. data/test/lint/sorted_sets.rb +0 -316
  104. data/test/lint/strings.rb +0 -260
  105. data/test/lint/value_types.rb +0 -122
  106. data/test/persistence_control_commands_test.rb +0 -26
  107. data/test/pipelining_commands_test.rb +0 -242
  108. data/test/publish_subscribe_test.rb +0 -282
  109. data/test/remote_server_control_commands_test.rb +0 -118
  110. data/test/scanning_test.rb +0 -413
  111. data/test/scripting_test.rb +0 -78
  112. data/test/sentinel_command_test.rb +0 -80
  113. data/test/sentinel_test.rb +0 -255
  114. data/test/sorting_test.rb +0 -59
  115. data/test/ssl_test.rb +0 -73
  116. data/test/support/connection/hiredis.rb +0 -1
  117. data/test/support/connection/ruby.rb +0 -1
  118. data/test/support/connection/synchrony.rb +0 -17
  119. data/test/support/redis_mock.rb +0 -130
  120. data/test/support/ssl/gen_certs.sh +0 -31
  121. data/test/support/ssl/trusted-ca.crt +0 -25
  122. data/test/support/ssl/trusted-ca.key +0 -27
  123. data/test/support/ssl/trusted-cert.crt +0 -81
  124. data/test/support/ssl/trusted-cert.key +0 -28
  125. data/test/support/ssl/untrusted-ca.crt +0 -26
  126. data/test/support/ssl/untrusted-ca.key +0 -27
  127. data/test/support/ssl/untrusted-cert.crt +0 -82
  128. data/test/support/ssl/untrusted-cert.key +0 -28
  129. data/test/support/wire/synchrony.rb +0 -24
  130. data/test/support/wire/thread.rb +0 -5
  131. data/test/synchrony_driver.rb +0 -88
  132. data/test/test.conf.erb +0 -9
  133. data/test/thread_safety_test.rb +0 -62
  134. data/test/transactions_test.rb +0 -264
  135. data/test/unknown_commands_test.rb +0 -14
  136. data/test/url_param_test.rb +0 -138
data/lib/redis/client.rb CHANGED
@@ -1,589 +1,127 @@
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
-
96
- def connect
97
- @pid = Process.pid
98
25
 
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)
106
28
  end
107
-
108
- self
109
29
  end
110
30
 
111
31
  def id
112
- @options[:id] || "redis://#{location}/#{db}"
113
- end
114
-
115
- def location
116
- path || "#{host}:#{port}"
32
+ config.id
117
33
  end
118
34
 
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
127
- end
35
+ def server_url
36
+ config.server_url
128
37
  end
129
38
 
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
-
190
- @reconnect = false
191
-
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
197
- end
198
-
199
- raise exception if exception
200
- ensure
201
- @reconnect = reconnect
202
- end
203
-
204
- result
205
- end
206
-
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
39
+ def timeout
40
+ config.read_timeout
213
41
  end
214
42
 
215
- def call_without_timeout(command, &blk)
216
- call_with_timeout(command, 0, &blk)
43
+ def db
44
+ config.db
217
45
  end
218
46
 
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
47
+ def host
48
+ config.host unless config.path
234
49
  end
235
50
 
236
- def connected?
237
- !! (connection && connection.connected?)
51
+ def port
52
+ config.port unless config.path
238
53
  end
239
54
 
240
- def disconnect
241
- connection.disconnect if connected?
55
+ def path
56
+ config.path
242
57
  end
243
58
 
244
- def reconnect
245
- disconnect
246
- connect
59
+ def username
60
+ config.username
247
61
  end
248
62
 
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]
63
+ def password
64
+ config.password
258
65
  end
259
66
 
260
- def read
261
- io do
262
- value = connection.read
263
- @pending_reads -= 1
264
- value
265
- end
266
- end
67
+ undef_method :call
68
+ undef_method :call_once
69
+ undef_method :call_once_v
70
+ undef_method :blocking_call
267
71
 
268
- def write(command)
269
- io do
270
- @pending_reads += 1
271
- connection.write(command)
272
- end
72
+ def call_v(command, &block)
73
+ super(command, &block)
74
+ rescue ::RedisClient::Error => error
75
+ translate_error!(error)
273
76
  end
274
77
 
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?
78
+ def blocking_call_v(timeout, command, &block)
79
+ if timeout && timeout > 0
80
+ # Can't use the command timeout argument as the connection timeout
81
+ # otherwise it would be very racy. So we add an extra 100ms to account for
82
+ # the network delay.
83
+ timeout += 0.1
283
84
  end
284
- end
285
85
 
286
- def without_socket_timeout(&blk)
287
- with_socket_timeout(0, &blk)
86
+ super(timeout, command, &block)
87
+ rescue ::RedisClient::Error => error
88
+ translate_error!(error)
288
89
  end
289
90
 
290
- def with_reconnect(val=true)
291
- begin
292
- original, @reconnect = @reconnect, val
293
- yield
294
- ensure
295
- @reconnect = original
296
- end
91
+ def pipelined
92
+ super
93
+ rescue ::RedisClient::Error => error
94
+ translate_error!(error)
297
95
  end
298
96
 
299
- def without_reconnect(&blk)
300
- with_reconnect(false, &blk)
97
+ def multi
98
+ super
99
+ rescue ::RedisClient::Error => error
100
+ translate_error!(error)
301
101
  end
302
102
 
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
327
- end
103
+ def disable_reconnection(&block)
104
+ ensure_connected(retryable: false, &block)
328
105
  end
329
106
 
330
- def establish_connection
331
- server = @connector.resolve.dup
332
-
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})"
107
+ def inherit_socket!
108
+ @inherit_socket = true
346
109
  end
347
110
 
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
111
+ private
370
112
 
371
- if attempts <= @options[:reconnect_attempts] && @reconnect
372
- retry
373
- else
374
- raise
375
- end
376
- rescue Exception
377
- disconnect
378
- raise
379
- end
113
+ def translate_error!(error)
114
+ redis_error = translate_error_class(error.class)
115
+ raise redis_error, error.message, error.backtrace
380
116
  end
381
117
 
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)
118
+ def translate_error_class(error_class)
119
+ ERROR_MAPPING.fetch(error_class)
120
+ rescue IndexError
121
+ if (client_error = error_class.ancestors.find { |a| ERROR_MAPPING[a] })
122
+ ERROR_MAPPING[error_class] = ERROR_MAPPING[client_error]
432
123
  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
474
- end
475
-
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
489
- end
490
-
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
124
+ raise
587
125
  end
588
126
  end
589
127
  end