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