redis 4.2.5 → 5.0.7

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