redis 4.2.5 → 5.0.7

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 (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