redis 4.2.5 → 4.5.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 862e8133262fead707e4ca317b29d0e35425e3a7861ea1a52033bc91ba61bf6d
4
- data.tar.gz: 5b2b32250d783fe58b0af54cc386f2568241644a0badc0b7247bb29b1ac94a93
3
+ metadata.gz: b1e483240f70aa8a92a7fe1f38968262ea82e84a97e436a9c7c695c032584313
4
+ data.tar.gz: 1cb164b2d588ef2f2701b91371a12a0d90299a78994f0d614cb14ab0daa36243
5
5
  SHA512:
6
- metadata.gz: 2aab289f4f22b2f3a804ca7b0da4cf95e352a8d246611490d8d803edebbbc7e7c299355cd0b90e6f3ac8bc0d11e9bf3792a328c95847d32e1d984729afe66ed2
7
- data.tar.gz: d9ec8ba4d314d099e909cdddf6dfb4c3c14b853b46f45c4909ac3ba10fb1880bd9a7b0465257fa91f9a4ea6c9f82723c16c177810ac15c89fab3790d7af31ad0
6
+ metadata.gz: 72c3e91839061ae26e27cbb08330f752acbba5f440c8549cad4325b2c2f517a907a0db11330ca9ba221255aa7b570efe2050bde9cbb905217794c4754c53dadb
7
+ data.tar.gz: e92cc58111fb9e1553a3363c56569ef6064c3851263d5eb86147c907878650b697db5c598b2b2046f9c9025cc40ca7eda5ce16f4f21049ac49f09385cf80a92f
data/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Unreleased
2
2
 
3
+ # 4.5.0
4
+
5
+ * Handle parts of the command using incompatible encodings. See #1037.
6
+ * Add GET option to SET command. See #1036.
7
+ * Add ZRANDMEMBER command. See #1035.
8
+ * Add LMOVE/BLMOVE commands. See #1034.
9
+ * Add ZMSCORE command. See #1032.
10
+ * Add LT/GT options to ZADD. See #1033.
11
+ * Add SMISMEMBER command. See #1031.
12
+ * Add EXAT/PXAT options to SET. See #1028.
13
+ * Add GETDEL/GETEX commands. See #1024.
14
+ * `Redis#exists` now returns an Integer by default, as warned since 4.2.0. The old behavior can be restored with `Redis.exists_returns_integer = false`.
15
+ * Fix Redis < 6 detection during connect. See #1025.
16
+ * Fix fetching command details in Redis cluster when the first node is unhealthy. See #1026.
17
+
18
+ # 4.4.0
19
+
20
+ * Redis cluster: fix cross-slot validation in pipelines. Fix ##1019.
21
+ * Add support for `XAUTOCLAIM`. See #1018.
22
+ * Properly issue `READONLY` when reconnecting to replicas. Fix #1017.
23
+ * Make `del` a noop if passed an empty list of keys. See #998.
24
+ * Add support for `ZINTER`. See #995.
25
+
26
+ # 4.3.1
27
+
28
+ * Fix password authentication against redis server 5 and older.
29
+
30
+ # 4.3.0
31
+
32
+ * Add the TYPE argument to scan and scan_each. See #985.
33
+ * Support AUTH command for ACL. See #967.
34
+
3
35
  # 4.2.5
4
36
 
5
37
  * Optimize the ruby connector write buffering. See #964.
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # redis-rb [![Build Status][travis-image]][travis-link] [![Inline docs][inchpages-image]][inchpages-link] ![](https://github.com/redis/redis-rb/workflows/Test/badge.svg?branch=master)
1
+ # redis-rb [![Build Status][gh-actions-image]][gh-actions-link] [![Inline docs][inchpages-image]][inchpages-link]
2
2
 
3
3
  A Ruby client that tries to match [Redis][redis-home]' API one-to-one, while still
4
4
  providing an idiomatic interface.
@@ -54,6 +54,12 @@ To connect to a password protected Redis instance, use:
54
54
  redis = Redis.new(password: "mysecret")
55
55
  ```
56
56
 
57
+ To connect a Redis instance using [ACL](https://redis.io/topics/acl), use:
58
+
59
+ ```ruby
60
+ redis = Redis.new(username: 'myname', password: 'mysecret')
61
+ ```
62
+
57
63
  The Redis class exports methods that are named identical to the commands
58
64
  they execute. The arguments these methods accept are often identical to
59
65
  the arguments specified on the [Redis website][redis-commands]. For
@@ -440,7 +446,7 @@ redis = Redis.new(:driver => :synchrony)
440
446
  ## Testing
441
447
 
442
448
  This library is tested against recent Ruby and Redis versions.
443
- Check [Travis][travis-link] for the exact versions supported.
449
+ Check [Github Actions][gh-actions-link] for the exact versions supported.
444
450
 
445
451
  ## See Also
446
452
 
@@ -459,12 +465,11 @@ client and evangelized Redis in Rubyland. Thank you, Ezra.
459
465
  requests.
460
466
 
461
467
 
462
- [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
463
- [inchpages-link]: https://inch-ci.org/github/redis/redis-rb
464
- [redis-commands]: https://redis.io/commands
465
- [redis-home]: https://redis.io
466
- [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
467
- [travis-home]: https://travis-ci.org/
468
- [travis-image]: https://secure.travis-ci.org/redis/redis-rb.svg?branch=master
469
- [travis-link]: https://travis-ci.org/redis/redis-rb
470
- [rubydoc]: http://www.rubydoc.info/gems/redis
468
+ [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
469
+ [inchpages-link]: https://inch-ci.org/github/redis/redis-rb
470
+ [redis-commands]: https://redis.io/commands
471
+ [redis-home]: https://redis.io
472
+ [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
473
+ [gh-actions-image]: https://github.com/redis/redis-rb/workflows/Test/badge.svg
474
+ [gh-actions-link]: https://github.com/redis/redis-rb/actions
475
+ [rubydoc]: http://www.rubydoc.info/gems/redis
data/lib/redis/client.rb CHANGED
@@ -17,6 +17,7 @@ class Redis
17
17
  write_timeout: nil,
18
18
  connect_timeout: nil,
19
19
  timeout: 5.0,
20
+ username: nil,
20
21
  password: nil,
21
22
  db: 0,
22
23
  driver: nil,
@@ -61,6 +62,10 @@ class Redis
61
62
  @options[:read_timeout]
62
63
  end
63
64
 
65
+ def username
66
+ @options[:username]
67
+ end
68
+
64
69
  def password
65
70
  @options[:password]
66
71
  end
@@ -110,7 +115,23 @@ class Redis
110
115
  # Don't try to reconnect when the connection is fresh
111
116
  with_reconnect(false) do
112
117
  establish_connection
113
- call [:auth, password] if password
118
+ if password
119
+ if username
120
+ begin
121
+ call [:auth, username, password]
122
+ rescue CommandError => err # Likely on Redis < 6
123
+ if err.message.match?(/ERR wrong number of arguments for \'auth\' command/)
124
+ call [:auth, password]
125
+ else
126
+ raise
127
+ end
128
+ end
129
+ else
130
+ call [:auth, password]
131
+ end
132
+ end
133
+
134
+ call [:readonly] if @options[:readonly]
114
135
  call [:select, db] if db != 0
115
136
  call [:client, :setname, @options[:id]] if @options[:id]
116
137
  @connector.check(self)
@@ -131,7 +152,7 @@ class Redis
131
152
  reply = process([command]) { read }
132
153
  raise reply if reply.is_a?(CommandError)
133
154
 
134
- if block_given?
155
+ if block_given? && reply != 'QUEUED'
135
156
  yield reply
136
157
  else
137
158
  reply
@@ -434,7 +455,8 @@ class Redis
434
455
  defaults[:scheme] = uri.scheme
435
456
  defaults[:host] = uri.host if uri.host
436
457
  defaults[:port] = uri.port if uri.port
437
- defaults[:password] = CGI.unescape(uri.password) if uri.password
458
+ defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
459
+ defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
438
460
  defaults[:db] = uri.path[1..-1].to_i if uri.path
439
461
  defaults[:role] = :master
440
462
  else
@@ -510,7 +532,7 @@ class Redis
510
532
  require_relative "connection/#{driver}"
511
533
  rescue LoadError, NameError
512
534
  begin
513
- require "connection/#{driver}"
535
+ require "redis/connection/#{driver}"
514
536
  rescue LoadError, NameError => error
515
537
  raise "Cannot load driver #{driver.inspect}: #{error.message}"
516
538
  end
@@ -579,6 +601,7 @@ class Redis
579
601
  client = Client.new(@options.merge({
580
602
  host: sentinel[:host] || sentinel["host"],
581
603
  port: sentinel[:port] || sentinel["port"],
604
+ username: sentinel[:username] || sentinel["username"],
582
605
  password: sentinel[:password] || sentinel["password"],
583
606
  reconnect_attempts: 0
584
607
  }))
@@ -10,22 +10,21 @@ class Redis
10
10
  module_function
11
11
 
12
12
  def load(nodes)
13
- details = {}
14
-
15
13
  nodes.each do |node|
16
- details = fetch_command_details(node)
17
- details.empty? ? next : break
14
+ begin
15
+ return fetch_command_details(node)
16
+ rescue CannotConnectError, ConnectionError, CommandError
17
+ next # can retry on another node
18
+ end
18
19
  end
19
20
 
20
- details
21
+ raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
21
22
  end
22
23
 
23
24
  def fetch_command_details(node)
24
25
  node.call(%i[command]).map do |reply|
25
26
  [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
26
27
  end.to_h
27
- rescue CannotConnectError, ConnectionError, CommandError
28
- {} # can retry on another node
29
28
  end
30
29
 
31
30
  private_class_method :fetch_command_details
@@ -76,8 +76,9 @@ class Redis
76
76
  clients = options.map do |node_key, option|
77
77
  next if replica_disabled? && slave?(node_key)
78
78
 
79
+ option = option.merge(readonly: true) if slave?(node_key)
80
+
79
81
  client = Client.new(option)
80
- client.call(%i[readonly]) if slave?(node_key)
81
82
  [node_key, client]
82
83
  end
83
84
 
@@ -18,6 +18,7 @@ class Redis
18
18
  @node_opts = build_node_options(node_addrs)
19
19
  @replica = options.delete(:replica) == true
20
20
  add_common_node_option_if_needed(options, @node_opts, :scheme)
21
+ add_common_node_option_if_needed(options, @node_opts, :username)
21
22
  add_common_node_option_if_needed(options, @node_opts, :password)
22
23
  @options = options
23
24
  end
@@ -63,7 +64,9 @@ class Redis
63
64
  raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
64
65
 
65
66
  db = uri.path.split('/')[1]&.to_i
66
- { scheme: uri.scheme, password: uri.password, host: uri.host, port: uri.port, db: db }.reject { |_, v| v.nil? }
67
+
68
+ { scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
69
+ .reject { |_, v| v.nil? || v == '' }
67
70
  rescue URI::InvalidURIError => err
68
71
  raise InvalidClientOptionError, err.message
69
72
  end
@@ -79,7 +82,7 @@ class Redis
79
82
 
80
83
  # Redis cluster node returns only host and port information.
81
84
  # So we should complement additional information such as:
82
- # scheme, password and so on.
85
+ # scheme, username, password and so on.
83
86
  def add_common_node_option_if_needed(options, node_opts, key)
84
87
  return options if options[key].nil? && node_opts.first[key].nil?
85
88
 
data/lib/redis/cluster.rb CHANGED
@@ -78,11 +78,13 @@ class Redis
78
78
  end
79
79
 
80
80
  def call_pipeline(pipeline)
81
- node_keys, command_keys = extract_keys_in_pipeline(pipeline)
82
- raise CrossSlotPipeliningError, command_keys if node_keys.size > 1
81
+ node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq
82
+ if node_keys.size > 1
83
+ raise(CrossSlotPipeliningError,
84
+ pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
85
+ end
83
86
 
84
- node = find_node(node_keys.first)
85
- try_send(node, :call_pipeline, pipeline)
87
+ try_send(find_node(node_keys.first), :call_pipeline, pipeline)
86
88
  end
87
89
 
88
90
  def call_with_timeout(command, timeout, &block)
@@ -128,7 +130,7 @@ class Redis
128
130
  def send_command(command, &block)
129
131
  cmd = command.first.to_s.downcase
130
132
  case cmd
131
- when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
133
+ when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
132
134
  @node.call_all(command, &block).first
133
135
  when 'flushall', 'flushdb'
134
136
  @node.call_master(command, &block).first
@@ -253,14 +255,14 @@ class Redis
253
255
  find_node(node_key)
254
256
  end
255
257
 
256
- def find_node_key(command)
258
+ def find_node_key(command, primary_only: false)
257
259
  key = @command.extract_first_key(command)
258
260
  return if key.empty?
259
261
 
260
262
  slot = KeySlotConverter.convert(key)
261
263
  return unless @slot.exists?(slot)
262
264
 
263
- if @command.should_send_to_master?(command)
265
+ if @command.should_send_to_master?(command) || primary_only
264
266
  @slot.find_node_key_of_master(slot)
265
267
  else
266
268
  @slot.find_node_key_of_slave(slot)
@@ -285,11 +287,5 @@ class Redis
285
287
  @node.map(&:disconnect)
286
288
  @node, @slot = fetch_cluster_info!(@option)
287
289
  end
288
-
289
- def extract_keys_in_pipeline(pipeline)
290
- node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
291
- command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
292
- [node_keys, command_keys]
293
- end
294
290
  end
295
291
  end
@@ -12,11 +12,13 @@ class Redis
12
12
  if i.is_a? Array
13
13
  i.each do |j|
14
14
  j = j.to_s
15
+ j = j.encoding == Encoding::BINARY ? j : j.b
15
16
  command << "$#{j.bytesize}"
16
17
  command << j
17
18
  end
18
19
  else
19
20
  i = i.to_s
21
+ i = i.encoding == Encoding::BINARY ? i : i.b
20
22
  command << "$#{i.bytesize}"
21
23
  command << i
22
24
  end
@@ -32,12 +32,30 @@ class Redis
32
32
  @write_timeout = (timeout if timeout && timeout > 0)
33
33
  end
34
34
 
35
- def read(nbytes)
36
- result = @buffer.slice!(0, nbytes)
35
+ string_capacity_support = begin
36
+ String.new(capacity: 0)
37
+ true # Ruby 2.4+
38
+ rescue ArgumentError
39
+ false # Ruby 2.3
40
+ end
41
+
42
+ if string_capacity_support
43
+ def read(nbytes)
44
+ result = @buffer.slice!(0, nbytes)
37
45
 
38
- result << _read_from_socket(nbytes - result.bytesize) while result.bytesize < nbytes
46
+ buffer = String.new(capacity: nbytes, encoding: Encoding::ASCII_8BIT)
47
+ result << _read_from_socket(nbytes - result.bytesize, buffer) while result.bytesize < nbytes
39
48
 
40
- result
49
+ result
50
+ end
51
+ else
52
+ def read(nbytes)
53
+ result = @buffer.slice!(0, nbytes)
54
+
55
+ result << _read_from_socket(nbytes - result.bytesize, "".b) while result.bytesize < nbytes
56
+
57
+ result
58
+ end
41
59
  end
42
60
 
43
61
  def gets
@@ -48,9 +66,9 @@ class Redis
48
66
  @buffer.slice!(0, crlf + CRLF.bytesize)
49
67
  end
50
68
 
51
- def _read_from_socket(nbytes)
69
+ def _read_from_socket(nbytes, buffer = nil)
52
70
  loop do
53
- case chunk = read_nonblock(nbytes, exception: false)
71
+ case chunk = read_nonblock(nbytes, buffer, exception: false)
54
72
  when :wait_readable
55
73
  unless wait_readable(@timeout)
56
74
  raise Redis::TimeoutError
@@ -316,6 +316,16 @@ class Redis
316
316
  node_for(key).get(key)
317
317
  end
318
318
 
319
+ # Get the value of a key and delete it.
320
+ def getdel(key)
321
+ node_for(key).getdel(key)
322
+ end
323
+
324
+ # Get the value of a key and sets its time to live based on options.
325
+ def getex(key, **options)
326
+ node_for(key).getex(key, **options)
327
+ end
328
+
319
329
  # Get the values of all the given keys as an Array.
320
330
  def mget(*keys)
321
331
  mapped_mget(*keys).values_at(*keys)
@@ -393,6 +403,21 @@ class Redis
393
403
  node_for(key).llen(key)
394
404
  end
395
405
 
406
+ # Remove the first/last element in a list, append/prepend it to another list and return it.
407
+ def lmove(source, destination, where_source, where_destination)
408
+ ensure_same_node(:lmove, [source, destination]) do |node|
409
+ node.lmove(source, destination, where_source, where_destination)
410
+ end
411
+ end
412
+
413
+ # Remove the first/last element in a list and append/prepend it
414
+ # to another list and return it, or block until one is available.
415
+ def blmove(source, destination, where_source, where_destination, timeout: 0)
416
+ ensure_same_node(:lmove, [source, destination]) do |node|
417
+ node.blmove(source, destination, where_source, where_destination, timeout: timeout)
418
+ end
419
+ end
420
+
396
421
  # Prepend one or more values to a list.
397
422
  def lpush(key, value)
398
423
  node_for(key).lpush(key, value)
@@ -413,14 +438,14 @@ class Redis
413
438
  node_for(key).rpushx(key, value)
414
439
  end
415
440
 
416
- # Remove and get the first element in a list.
417
- def lpop(key)
418
- node_for(key).lpop(key)
441
+ # Remove and get the first elements in a list.
442
+ def lpop(key, count = nil)
443
+ node_for(key).lpop(key, count)
419
444
  end
420
445
 
421
- # Remove and get the last element in a list.
422
- def rpop(key)
423
- node_for(key).rpop(key)
446
+ # Remove and get the last elements in a list.
447
+ def rpop(key, count = nil)
448
+ node_for(key).rpop(key, count)
424
449
  end
425
450
 
426
451
  # Remove the last element in a list, append it to another list and return
@@ -542,6 +567,11 @@ class Redis
542
567
  node_for(key).sismember(key, member)
543
568
  end
544
569
 
570
+ # Determine if multiple values are members of a set.
571
+ def smismember(key, *members)
572
+ node_for(key).smismember(key, *members)
573
+ end
574
+
545
575
  # Get all the members in a set.
546
576
  def smembers(key)
547
577
  node_for(key).smembers(key)
@@ -626,6 +656,16 @@ class Redis
626
656
  node_for(key).zscore(key, member)
627
657
  end
628
658
 
659
+ # Get one or more random members from a sorted set.
660
+ def zrandmember(key, count = nil, **options)
661
+ node_for(key).zrandmember(key, count, **options)
662
+ end
663
+
664
+ # Get the scores associated with the given members in a sorted set.
665
+ def zmscore(key, *members)
666
+ node_for(key).zmscore(key, *members)
667
+ end
668
+
629
669
  # Return a range of members in a sorted set, by index.
630
670
  def zrange(key, start, stop, **options)
631
671
  node_for(key).zrange(key, start, stop, **options)
@@ -674,6 +714,13 @@ class Redis
674
714
  node_for(key).zcount(key, min, max)
675
715
  end
676
716
 
717
+ # Get the intersection of multiple sorted sets
718
+ def zinter(*keys, **options)
719
+ ensure_same_node(:zinter, keys) do |node|
720
+ node.zinter(*keys, **options)
721
+ end
722
+ end
723
+
677
724
  # Intersect multiple sorted sets and store the resulting sorted set in a new
678
725
  # key.
679
726
  def zinterstore(destination, keys, **options)
data/lib/redis/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Redis
4
- VERSION = '4.2.5'
4
+ VERSION = '4.5.0'
5
5
  end
data/lib/redis.rb CHANGED
@@ -4,6 +4,8 @@ require "monitor"
4
4
  require_relative "redis/errors"
5
5
 
6
6
  class Redis
7
+ @exists_returns_integer = true
8
+
7
9
  class << self
8
10
  attr_reader :exists_returns_integer
9
11
 
@@ -39,6 +41,7 @@ class Redis
39
41
  # @option options [String] :path path to server socket (overrides host and port)
40
42
  # @option options [Float] :timeout (5.0) timeout in seconds
41
43
  # @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds
44
+ # @option options [String] :username Username to authenticate against server
42
45
  # @option options [String] :password Password to authenticate against server
43
46
  # @option options [Integer] :db (0) Database to select after initial connect
44
47
  # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`, `:synchrony`
@@ -143,12 +146,13 @@ class Redis
143
146
 
144
147
  # Authenticate to the server.
145
148
  #
146
- # @param [String] password must match the password specified in the
147
- # `requirepass` directive in the configuration file
149
+ # @param [Array<String>] args includes both username and password
150
+ # or only password
148
151
  # @return [String] `OK`
149
- def auth(password)
152
+ # @see https://redis.io/commands/auth AUTH command
153
+ def auth(*args)
150
154
  synchronize do |client|
151
- client.call([:auth, password])
155
+ client.call([:auth, *args])
152
156
  end
153
157
  end
154
158
 
@@ -553,6 +557,9 @@ class Redis
553
557
  # @param [String, Array<String>] keys
554
558
  # @return [Integer] number of keys that were deleted
555
559
  def del(*keys)
560
+ keys.flatten!(1)
561
+ return 0 if keys.empty?
562
+
556
563
  synchronize do |client|
557
564
  client.call([:del] + keys)
558
565
  end
@@ -829,17 +836,23 @@ class Redis
829
836
  # @param [Hash] options
830
837
  # - `:ex => Integer`: Set the specified expire time, in seconds.
831
838
  # - `:px => Integer`: Set the specified expire time, in milliseconds.
839
+ # - `:exat => Integer` : Set the specified Unix time at which the key will expire, in seconds.
840
+ # - `:pxat => Integer` : Set the specified Unix time at which the key will expire, in milliseconds.
832
841
  # - `:nx => true`: Only set the key if it does not already exist.
833
842
  # - `:xx => true`: Only set the key if it already exist.
834
843
  # - `:keepttl => true`: Retain the time to live associated with the key.
844
+ # - `:get => true`: Return the old string stored at key, or nil if key did not exist.
835
845
  # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
836
- def set(key, value, ex: nil, px: nil, nx: nil, xx: nil, keepttl: nil)
846
+ def set(key, value, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil, keepttl: nil, get: nil)
837
847
  args = [:set, key, value.to_s]
838
848
  args << "EX" << ex if ex
839
849
  args << "PX" << px if px
850
+ args << "EXAT" << exat if exat
851
+ args << "PXAT" << pxat if pxat
840
852
  args << "NX" if nx
841
853
  args << "XX" if xx
842
854
  args << "KEEPTTL" if keepttl
855
+ args << "GET" if get
843
856
 
844
857
  synchronize do |client|
845
858
  if nx || xx
@@ -1105,6 +1118,45 @@ class Redis
1105
1118
  end
1106
1119
  end
1107
1120
 
1121
+ # Get the value of key and delete the key. This command is similar to GET,
1122
+ # except for the fact that it also deletes the key on success.
1123
+ #
1124
+ # @param [String] key
1125
+ # @return [String] the old value stored in the key, or `nil` if the key
1126
+ # did not exist
1127
+ def getdel(key)
1128
+ synchronize do |client|
1129
+ client.call([:getdel, key])
1130
+ end
1131
+ end
1132
+
1133
+ # Get the value of key and optionally set its expiration. GETEX is similar to
1134
+ # GET, but is a write command with additional options. When no options are
1135
+ # provided, GETEX behaves like GET.
1136
+ #
1137
+ # @param [String] key
1138
+ # @param [Hash] options
1139
+ # - `:ex => Integer`: Set the specified expire time, in seconds.
1140
+ # - `:px => Integer`: Set the specified expire time, in milliseconds.
1141
+ # - `:exat => true`: Set the specified Unix time at which the key will
1142
+ # expire, in seconds.
1143
+ # - `:pxat => true`: Set the specified Unix time at which the key will
1144
+ # expire, in milliseconds.
1145
+ # - `:persist => true`: Remove the time to live associated with the key.
1146
+ # @return [String] The value of key, or nil when key does not exist.
1147
+ def getex(key, ex: nil, px: nil, exat: nil, pxat: nil, persist: false)
1148
+ args = [:getex, key]
1149
+ args << "EX" << ex if ex
1150
+ args << "PX" << px if px
1151
+ args << "EXAT" << exat if exat
1152
+ args << "PXAT" << pxat if pxat
1153
+ args << "PERSIST" if persist
1154
+
1155
+ synchronize do |client|
1156
+ client.call(args)
1157
+ end
1158
+ end
1159
+
1108
1160
  # Get the length of the value stored in a key.
1109
1161
  #
1110
1162
  # @param [String] key
@@ -1126,6 +1178,59 @@ class Redis
1126
1178
  end
1127
1179
  end
1128
1180
 
1181
+ # Remove the first/last element in a list, append/prepend it to another list and return it.
1182
+ #
1183
+ # @param [String] source source key
1184
+ # @param [String] destination destination key
1185
+ # @param [String, Symbol] where_source from where to remove the element from the source list
1186
+ # e.g. 'LEFT' - from head, 'RIGHT' - from tail
1187
+ # @param [String, Symbol] where_destination where to push the element to the source list
1188
+ # e.g. 'LEFT' - to head, 'RIGHT' - to tail
1189
+ #
1190
+ # @return [nil, String] the element, or nil when the source key does not exist
1191
+ #
1192
+ # @note This command comes in place of the now deprecated RPOPLPUSH.
1193
+ # Doing LMOVE RIGHT LEFT is equivalent.
1194
+ def lmove(source, destination, where_source, where_destination)
1195
+ where_source, where_destination = _normalize_move_wheres(where_source, where_destination)
1196
+
1197
+ synchronize do |client|
1198
+ client.call([:lmove, source, destination, where_source, where_destination])
1199
+ end
1200
+ end
1201
+
1202
+ # Remove the first/last element in a list and append/prepend it
1203
+ # to another list and return it, or block until one is available.
1204
+ #
1205
+ # @example With timeout
1206
+ # element = redis.blmove("foo", "bar", "LEFT", "RIGHT", timeout: 5)
1207
+ # # => nil on timeout
1208
+ # # => "element" on success
1209
+ # @example Without timeout
1210
+ # element = redis.blmove("foo", "bar", "LEFT", "RIGHT")
1211
+ # # => "element"
1212
+ #
1213
+ # @param [String] source source key
1214
+ # @param [String] destination destination key
1215
+ # @param [String, Symbol] where_source from where to remove the element from the source list
1216
+ # e.g. 'LEFT' - from head, 'RIGHT' - from tail
1217
+ # @param [String, Symbol] where_destination where to push the element to the source list
1218
+ # e.g. 'LEFT' - to head, 'RIGHT' - to tail
1219
+ # @param [Hash] options
1220
+ # - `:timeout => Numeric`: timeout in seconds, defaults to no timeout
1221
+ #
1222
+ # @return [nil, String] the element, or nil when the source key does not exist or the timeout expired
1223
+ #
1224
+ def blmove(source, destination, where_source, where_destination, timeout: 0)
1225
+ where_source, where_destination = _normalize_move_wheres(where_source, where_destination)
1226
+
1227
+ synchronize do |client|
1228
+ command = [:blmove, source, destination, where_source, where_destination, timeout]
1229
+ timeout += client.timeout if timeout > 0
1230
+ client.call_with_timeout(command, timeout)
1231
+ end
1232
+ end
1233
+
1129
1234
  # Prepend one or more values to a list, creating the list if it doesn't exist
1130
1235
  #
1131
1236
  # @param [String] key
@@ -1170,23 +1275,29 @@ class Redis
1170
1275
  end
1171
1276
  end
1172
1277
 
1173
- # Remove and get the first element in a list.
1278
+ # Remove and get the first elements in a list.
1174
1279
  #
1175
1280
  # @param [String] key
1176
- # @return [String]
1177
- def lpop(key)
1281
+ # @param [Integer] count number of elements to remove
1282
+ # @return [String, Array<String>] the values of the first elements
1283
+ def lpop(key, count = nil)
1178
1284
  synchronize do |client|
1179
- client.call([:lpop, key])
1285
+ command = [:lpop, key]
1286
+ command << count if count
1287
+ client.call(command)
1180
1288
  end
1181
1289
  end
1182
1290
 
1183
- # Remove and get the last element in a list.
1291
+ # Remove and get the last elements in a list.
1184
1292
  #
1185
1293
  # @param [String] key
1186
- # @return [String]
1187
- def rpop(key)
1294
+ # @param [Integer] count number of elements to remove
1295
+ # @return [String, Array<String>] the values of the last elements
1296
+ def rpop(key, count = nil)
1188
1297
  synchronize do |client|
1189
- client.call([:rpop, key])
1298
+ command = [:rpop, key]
1299
+ command << count if count
1300
+ client.call(command)
1190
1301
  end
1191
1302
  end
1192
1303
 
@@ -1468,6 +1579,19 @@ class Redis
1468
1579
  end
1469
1580
  end
1470
1581
 
1582
+ # Determine if multiple values are members of a set.
1583
+ #
1584
+ # @param [String] key
1585
+ # @param [String, Array<String>] members
1586
+ # @return [Array<Boolean>]
1587
+ def smismember(key, *members)
1588
+ synchronize do |client|
1589
+ client.call([:smismember, key, *members]) do |reply|
1590
+ reply.map(&Boolify)
1591
+ end
1592
+ end
1593
+ end
1594
+
1471
1595
  # Get all the members in a set.
1472
1596
  #
1473
1597
  # @param [String] key
@@ -1572,6 +1696,10 @@ class Redis
1572
1696
  # add elements)
1573
1697
  # - `:nx => true`: Don't update already existing elements (always
1574
1698
  # add new elements)
1699
+ # - `:lt => true`: Only update existing elements if the new score
1700
+ # is less than the current score
1701
+ # - `:gt => true`: Only update existing elements if the new score
1702
+ # is greater than the current score
1575
1703
  # - `:ch => true`: Modify the return value from the number of new
1576
1704
  # elements added, to the total number of elements changed (CH is an
1577
1705
  # abbreviation of changed); changed elements are new elements added
@@ -1586,10 +1714,12 @@ class Redis
1586
1714
  # pairs that were **added** to the sorted set.
1587
1715
  # - `Float` when option :incr is specified, holding the score of the member
1588
1716
  # after incrementing it.
1589
- def zadd(key, *args, nx: nil, xx: nil, ch: nil, incr: nil)
1717
+ def zadd(key, *args, nx: nil, xx: nil, lt: nil, gt: nil, ch: nil, incr: nil)
1590
1718
  command = [:zadd, key]
1591
1719
  command << "NX" if nx
1592
1720
  command << "XX" if xx
1721
+ command << "LT" if lt
1722
+ command << "GT" if gt
1593
1723
  command << "CH" if ch
1594
1724
  command << "INCR" if incr
1595
1725
 
@@ -1752,6 +1882,63 @@ class Redis
1752
1882
  end
1753
1883
  end
1754
1884
 
1885
+ # Get the scores associated with the given members in a sorted set.
1886
+ #
1887
+ # @example Get the scores for members "a" and "b"
1888
+ # redis.zmscore("zset", "a", "b")
1889
+ # # => [32.0, 48.0]
1890
+ #
1891
+ # @param [String] key
1892
+ # @param [String, Array<String>] members
1893
+ # @return [Array<Float>] scores of the members
1894
+ def zmscore(key, *members)
1895
+ synchronize do |client|
1896
+ client.call([:zmscore, key, *members]) do |reply|
1897
+ reply.map(&Floatify)
1898
+ end
1899
+ end
1900
+ end
1901
+
1902
+ # Get one or more random members from a sorted set.
1903
+ #
1904
+ # @example Get one random member
1905
+ # redis.zrandmember("zset")
1906
+ # # => "a"
1907
+ # @example Get multiple random members
1908
+ # redis.zrandmember("zset", 2)
1909
+ # # => ["a", "b"]
1910
+ # @example Gem multiple random members with scores
1911
+ # redis.zrandmember("zset", 2, with_scores: true)
1912
+ # # => [["a", 2.0], ["b", 3.0]]
1913
+ #
1914
+ # @param [String] key
1915
+ # @param [Integer] count
1916
+ # @param [Hash] options
1917
+ # - `:with_scores => true`: include scores in output
1918
+ #
1919
+ # @return [nil, String, Array<String>, Array<[String, Float]>]
1920
+ # - when `key` does not exist or set is empty, `nil`
1921
+ # - when `count` is not specified, a member
1922
+ # - when `count` is specified and `:with_scores` is not specified, an array of members
1923
+ # - when `:with_scores` is specified, an array with `[member, score]` pairs
1924
+ def zrandmember(key, count = nil, withscores: false, with_scores: withscores)
1925
+ if with_scores && count.nil?
1926
+ raise ArgumentError, "count argument must be specified"
1927
+ end
1928
+
1929
+ args = [:zrandmember, key]
1930
+ args << count if count
1931
+
1932
+ if with_scores
1933
+ args << "WITHSCORES"
1934
+ block = FloatifyPairs
1935
+ end
1936
+
1937
+ synchronize do |client|
1938
+ client.call(args, &block)
1939
+ end
1940
+ end
1941
+
1755
1942
  # Return a range of members in a sorted set, by index.
1756
1943
  #
1757
1944
  # @example Retrieve all members from a sorted set
@@ -2054,6 +2241,45 @@ class Redis
2054
2241
  end
2055
2242
  end
2056
2243
 
2244
+ # Return the intersection of multiple sorted sets
2245
+ #
2246
+ # @example Retrieve the intersection of `2*zsetA` and `1*zsetB`
2247
+ # redis.zinter("zsetA", "zsetB", :weights => [2.0, 1.0])
2248
+ # # => ["v1", "v2"]
2249
+ # @example Retrieve the intersection of `2*zsetA` and `1*zsetB`, and their scores
2250
+ # redis.zinter("zsetA", "zsetB", :weights => [2.0, 1.0], :with_scores => true)
2251
+ # # => [["v1", 3.0], ["v2", 6.0]]
2252
+ #
2253
+ # @param [String, Array<String>] keys one or more keys to intersect
2254
+ # @param [Hash] options
2255
+ # - `:weights => [Float, Float, ...]`: weights to associate with source
2256
+ # sorted sets
2257
+ # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
2258
+ # - `:with_scores => true`: include scores in output
2259
+ #
2260
+ # @return [Array<String>, Array<[String, Float]>]
2261
+ # - when `:with_scores` is not specified, an array of members
2262
+ # - when `:with_scores` is specified, an array with `[member, score]` pairs
2263
+ def zinter(*keys, weights: nil, aggregate: nil, with_scores: false)
2264
+ args = [:zinter, keys.size, *keys]
2265
+
2266
+ if weights
2267
+ args << "WEIGHTS"
2268
+ args.concat(weights)
2269
+ end
2270
+
2271
+ args << "AGGREGATE" << aggregate if aggregate
2272
+
2273
+ if with_scores
2274
+ args << "WITHSCORES"
2275
+ block = FloatifyPairs
2276
+ end
2277
+
2278
+ synchronize do |client|
2279
+ client.call(args, &block)
2280
+ end
2281
+ end
2282
+
2057
2283
  # Intersect multiple sorted sets and store the resulting sorted set in a new
2058
2284
  # key.
2059
2285
  #
@@ -2636,12 +2862,13 @@ class Redis
2636
2862
  _eval(:evalsha, args)
2637
2863
  end
2638
2864
 
2639
- def _scan(command, cursor, args, match: nil, count: nil, &block)
2865
+ def _scan(command, cursor, args, match: nil, count: nil, type: nil, &block)
2640
2866
  # SSCAN/ZSCAN/HSCAN already prepend the key to +args+.
2641
2867
 
2642
2868
  args << cursor
2643
2869
  args << "MATCH" << match if match
2644
2870
  args << "COUNT" << count if count
2871
+ args << "TYPE" << type if type
2645
2872
 
2646
2873
  synchronize do |client|
2647
2874
  client.call([command] + args, &block)
@@ -2656,11 +2883,15 @@ class Redis
2656
2883
  # @example Retrieve a batch of keys matching a pattern
2657
2884
  # redis.scan(4, :match => "key:1?")
2658
2885
  # # => ["92", ["key:13", "key:18"]]
2886
+ # @example Retrieve a batch of keys of a certain type
2887
+ # redis.scan(92, :type => "zset")
2888
+ # # => ["173", ["sortedset:14", "sortedset:78"]]
2659
2889
  #
2660
2890
  # @param [String, Integer] cursor the cursor of the iteration
2661
2891
  # @param [Hash] options
2662
2892
  # - `:match => String`: only return keys matching the pattern
2663
2893
  # - `:count => Integer`: return count keys at most per iteration
2894
+ # - `:type => String`: return keys only of the given type
2664
2895
  #
2665
2896
  # @return [String, Array<String>] the next cursor and all found keys
2666
2897
  def scan(cursor, **options)
@@ -2676,10 +2907,15 @@ class Redis
2676
2907
  # redis.scan_each(:match => "key:1?") {|key| puts key}
2677
2908
  # # => key:13
2678
2909
  # # => key:18
2910
+ # @example Execute block for each key of a type
2911
+ # redis.scan_each(:type => "hash") {|key| puts redis.type(key)}
2912
+ # # => "hash"
2913
+ # # => "hash"
2679
2914
  #
2680
2915
  # @param [Hash] options
2681
2916
  # - `:match => String`: only return keys matching the pattern
2682
2917
  # - `:count => Integer`: return count keys at most per iteration
2918
+ # - `:type => String`: return keys only of the given type
2683
2919
  #
2684
2920
  # @return [Enumerator] an enumerator for all found keys
2685
2921
  def scan_each(**options, &block)
@@ -3220,6 +3456,38 @@ class Redis
3220
3456
  synchronize { |client| client.call(args, &blk) }
3221
3457
  end
3222
3458
 
3459
+ # Transfers ownership of pending stream entries that match the specified criteria.
3460
+ #
3461
+ # @example Claim next pending message stuck > 5 minutes and mark as retry
3462
+ # redis.xautoclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0')
3463
+ # @example Claim 50 next pending messages stuck > 5 minutes and mark as retry
3464
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0', count: 50)
3465
+ # @example Claim next pending message stuck > 5 minutes and don't mark as retry
3466
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0', justid: true)
3467
+ # @example Claim next pending message after this id stuck > 5 minutes and mark as retry
3468
+ # redis.xautoclaim('mystream', 'mygroup', 'consumer1', 3600000, '1641321233-0')
3469
+ #
3470
+ # @param key [String] the stream key
3471
+ # @param group [String] the consumer group name
3472
+ # @param consumer [String] the consumer name
3473
+ # @param min_idle_time [Integer] the number of milliseconds
3474
+ # @param start [String] entry id to start scanning from or 0-0 for everything
3475
+ # @param count [Integer] number of messages to claim (default 1)
3476
+ # @param justid [Boolean] whether to fetch just an array of entry ids or not.
3477
+ # Does not increment retry count when true
3478
+ #
3479
+ # @return [Hash{String => Hash}] the entries successfully claimed
3480
+ # @return [Array<String>] the entry ids successfully claimed if justid option is `true`
3481
+ def xautoclaim(key, group, consumer, min_idle_time, start, count: nil, justid: false)
3482
+ args = [:xautoclaim, key, group, consumer, min_idle_time, start]
3483
+ if count
3484
+ args << 'COUNT' << count.to_s
3485
+ end
3486
+ args << 'JUSTID' if justid
3487
+ blk = justid ? HashifyStreamAutoclaimJustId : HashifyStreamAutoclaim
3488
+ synchronize { |client| client.call(args, &blk) }
3489
+ end
3490
+
3223
3491
  # Fetches not acknowledging pending entries
3224
3492
  #
3225
3493
  # @example With key and group
@@ -3426,10 +3694,24 @@ class Redis
3426
3694
 
3427
3695
  HashifyStreamEntries = lambda { |reply|
3428
3696
  reply.compact.map do |entry_id, values|
3429
- [entry_id, values.each_slice(2).to_h]
3697
+ [entry_id, values&.each_slice(2)&.to_h]
3430
3698
  end
3431
3699
  }
3432
3700
 
3701
+ HashifyStreamAutoclaim = lambda { |reply|
3702
+ {
3703
+ 'next' => reply[0],
3704
+ 'entries' => reply[1].map { |entry| [entry[0], entry[1].each_slice(2).to_h] }
3705
+ }
3706
+ }
3707
+
3708
+ HashifyStreamAutoclaimJustId = lambda { |reply|
3709
+ {
3710
+ 'next' => reply[0],
3711
+ 'entries' => reply[1]
3712
+ }
3713
+ }
3714
+
3433
3715
  HashifyStreamPendings = lambda { |reply|
3434
3716
  {
3435
3717
  'size' => reply[0],
@@ -3529,6 +3811,21 @@ class Redis
3529
3811
  end
3530
3812
  end
3531
3813
  end
3814
+
3815
+ def _normalize_move_wheres(where_source, where_destination)
3816
+ where_source = where_source.to_s.upcase
3817
+ where_destination = where_destination.to_s.upcase
3818
+
3819
+ if where_source != "LEFT" && where_source != "RIGHT"
3820
+ raise ArgumentError, "where_source must be 'LEFT' or 'RIGHT'"
3821
+ end
3822
+
3823
+ if where_destination != "LEFT" && where_destination != "RIGHT"
3824
+ raise ArgumentError, "where_destination must be 'LEFT' or 'RIGHT'"
3825
+ end
3826
+
3827
+ [where_source, where_destination]
3828
+ end
3532
3829
  end
3533
3830
 
3534
3831
  require_relative "redis/version"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: redis
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.2.5
4
+ version: 4.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ezra Zygmuntowicz
@@ -16,7 +16,7 @@ authors:
16
16
  autorequire:
17
17
  bindir: bin
18
18
  cert_chain: []
19
- date: 2020-11-20 00:00:00.000000000 Z
19
+ date: 2021-10-14 00:00:00.000000000 Z
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
22
  name: em-synchrony
@@ -102,9 +102,9 @@ licenses:
102
102
  metadata:
103
103
  bug_tracker_uri: https://github.com/redis/redis-rb/issues
104
104
  changelog_uri: https://github.com/redis/redis-rb/blob/master/CHANGELOG.md
105
- documentation_uri: https://www.rubydoc.info/gems/redis/4.2.5
105
+ documentation_uri: https://www.rubydoc.info/gems/redis/4.5.0
106
106
  homepage_uri: https://github.com/redis/redis-rb
107
- source_code_uri: https://github.com/redis/redis-rb/tree/v4.2.5
107
+ source_code_uri: https://github.com/redis/redis-rb/tree/v4.5.0
108
108
  post_install_message:
109
109
  rdoc_options: []
110
110
  require_paths:
@@ -113,7 +113,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
113
113
  requirements:
114
114
  - - ">="
115
115
  - !ruby/object:Gem::Version
116
- version: 2.3.0
116
+ version: 2.4.0
117
117
  required_rubygems_version: !ruby/object:Gem::Requirement
118
118
  requirements:
119
119
  - - ">="