redis 4.3.0 → 4.5.1

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: 5d173abb7a6c08e1feb87c9fd5ba33a2a0d907c6d045f6959d55f3234e56ceeb
4
- data.tar.gz: 7463d58522a3db5262eeea4b95834d82ede95cf102d2375e051cf4ada1b235c3
3
+ metadata.gz: 7a7232fef186e6d6a11a90d8dd9aa7c71114f017e0afe9378999a96c9e6b4e05
4
+ data.tar.gz: 689f176b87909bf61eb60e57d1eb795198162a7e039104c80facf36880964bda
5
5
  SHA512:
6
- metadata.gz: 0b34ab14e41e1bd63a99319f3ee5e1e7ea0c1e89581e525dfd82b77968a834525807b025f02d8ff5df4e7a994f19cc7ee4098103c70abd78ab6c22ec435a055c
7
- data.tar.gz: 84da776467bebb7fd59333084787e484203e55b81be5031c78b44741f2b79342cc1f6f48c1f9663b15f360a3af590783ee2f403da07a3e04486e35ba70e49e01
6
+ metadata.gz: 22b11dee92e298b46bb94a707156d3dbf9afb83c8e9e8cbf82cf366d582a7c1b7295d7f09a0fec01965245f4800c2482c0559d646f46ff1c6bba6423ab398ba9
7
+ data.tar.gz: 9a74ba29c8cb3d7634a44c78e30018a048e19cca66c7fad6a226722183f66546bebb370c92ecdf20522bb35ee72a09b2fda64891ff6d94db1702375bd1ba6b46
data/CHANGELOG.md CHANGED
@@ -1,5 +1,38 @@
1
1
  # Unreleased
2
2
 
3
+ # 4.5.1
4
+
5
+ * Restore the accidential auth behavior of redis-rb 4.3.0 with a warning. If provided with the `default` user's password, but a wrong username,
6
+ redis-rb will first try to connect as the provided user, but then will fallback to connect as the `default` user with the provided password.
7
+ This behavior is deprecated and will be removed in Redis 4.6.0. Fix #1038.
8
+
9
+ # 4.5.0
10
+
11
+ * Handle parts of the command using incompatible encodings. See #1037.
12
+ * Add GET option to SET command. See #1036.
13
+ * Add ZRANDMEMBER command. See #1035.
14
+ * Add LMOVE/BLMOVE commands. See #1034.
15
+ * Add ZMSCORE command. See #1032.
16
+ * Add LT/GT options to ZADD. See #1033.
17
+ * Add SMISMEMBER command. See #1031.
18
+ * Add EXAT/PXAT options to SET. See #1028.
19
+ * Add GETDEL/GETEX commands. See #1024.
20
+ * `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`.
21
+ * Fix Redis < 6 detection during connect. See #1025.
22
+ * Fix fetching command details in Redis cluster when the first node is unhealthy. See #1026.
23
+
24
+ # 4.4.0
25
+
26
+ * Redis cluster: fix cross-slot validation in pipelines. Fix ##1019.
27
+ * Add support for `XAUTOCLAIM`. See #1018.
28
+ * Properly issue `READONLY` when reconnecting to replicas. Fix #1017.
29
+ * Make `del` a noop if passed an empty list of keys. See #998.
30
+ * Add support for `ZINTER`. See #995.
31
+
32
+ # 4.3.1
33
+
34
+ * Fix password authentication against redis server 5 and older.
35
+
3
36
  # 4.3.0
4
37
 
5
38
  * Add the TYPE argument to scan and scan_each. See #985.
data/lib/redis/client.rb CHANGED
@@ -115,7 +115,33 @@ class Redis
115
115
  # Don't try to reconnect when the connection is fresh
116
116
  with_reconnect(false) do
117
117
  establish_connection
118
- call [:auth, username, password].compact if username || 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
+ elsif err.message.match?(/WRONGPASS invalid username-password pair/)
126
+ begin
127
+ call [:auth, password]
128
+ rescue CommandError
129
+ raise err
130
+ end
131
+ ::Kernel.warn(
132
+ "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
133
+ " the provided password was for the default user. This will start failing in redis-rb 4.6."
134
+ )
135
+ else
136
+ raise
137
+ end
138
+ end
139
+ else
140
+ call [:auth, password]
141
+ end
142
+ end
143
+
144
+ call [:readonly] if @options[:readonly]
119
145
  call [:select, db] if db != 0
120
146
  call [:client, :setname, @options[:id]] if @options[:id]
121
147
  @connector.check(self)
@@ -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
 
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)
@@ -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
@@ -21,7 +21,7 @@ class Redis
21
21
  super(*args)
22
22
 
23
23
  @timeout = @write_timeout = nil
24
- @buffer = "".dup
24
+ @buffer = "".b
25
25
  end
26
26
 
27
27
  def timeout=(timeout)
@@ -35,7 +35,8 @@ class Redis
35
35
  def read(nbytes)
36
36
  result = @buffer.slice!(0, nbytes)
37
37
 
38
- result << _read_from_socket(nbytes - result.bytesize) while result.bytesize < nbytes
38
+ buffer = String.new(capacity: nbytes, encoding: Encoding::ASCII_8BIT)
39
+ result << _read_from_socket(nbytes - result.bytesize, buffer) while result.bytesize < nbytes
39
40
 
40
41
  result
41
42
  end
@@ -48,9 +49,9 @@ class Redis
48
49
  @buffer.slice!(0, crlf + CRLF.bytesize)
49
50
  end
50
51
 
51
- def _read_from_socket(nbytes)
52
+ def _read_from_socket(nbytes, buffer = nil)
52
53
  loop do
53
- case chunk = read_nonblock(nbytes, exception: false)
54
+ case chunk = read_nonblock(nbytes, buffer, exception: false)
54
55
  when :wait_readable
55
56
  unless wait_readable(@timeout)
56
57
  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.3.0'
4
+ VERSION = '4.5.1'
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
 
@@ -555,6 +557,9 @@ class Redis
555
557
  # @param [String, Array<String>] keys
556
558
  # @return [Integer] number of keys that were deleted
557
559
  def del(*keys)
560
+ keys.flatten!(1)
561
+ return 0 if keys.empty?
562
+
558
563
  synchronize do |client|
559
564
  client.call([:del] + keys)
560
565
  end
@@ -831,17 +836,23 @@ class Redis
831
836
  # @param [Hash] options
832
837
  # - `:ex => Integer`: Set the specified expire time, in seconds.
833
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.
834
841
  # - `:nx => true`: Only set the key if it does not already exist.
835
842
  # - `:xx => true`: Only set the key if it already exist.
836
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.
837
845
  # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
838
- 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)
839
847
  args = [:set, key, value.to_s]
840
848
  args << "EX" << ex if ex
841
849
  args << "PX" << px if px
850
+ args << "EXAT" << exat if exat
851
+ args << "PXAT" << pxat if pxat
842
852
  args << "NX" if nx
843
853
  args << "XX" if xx
844
854
  args << "KEEPTTL" if keepttl
855
+ args << "GET" if get
845
856
 
846
857
  synchronize do |client|
847
858
  if nx || xx
@@ -1107,6 +1118,45 @@ class Redis
1107
1118
  end
1108
1119
  end
1109
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
+
1110
1160
  # Get the length of the value stored in a key.
1111
1161
  #
1112
1162
  # @param [String] key
@@ -1128,6 +1178,59 @@ class Redis
1128
1178
  end
1129
1179
  end
1130
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
+
1131
1234
  # Prepend one or more values to a list, creating the list if it doesn't exist
1132
1235
  #
1133
1236
  # @param [String] key
@@ -1172,23 +1275,29 @@ class Redis
1172
1275
  end
1173
1276
  end
1174
1277
 
1175
- # Remove and get the first element in a list.
1278
+ # Remove and get the first elements in a list.
1176
1279
  #
1177
1280
  # @param [String] key
1178
- # @return [String]
1179
- 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)
1180
1284
  synchronize do |client|
1181
- client.call([:lpop, key])
1285
+ command = [:lpop, key]
1286
+ command << count if count
1287
+ client.call(command)
1182
1288
  end
1183
1289
  end
1184
1290
 
1185
- # Remove and get the last element in a list.
1291
+ # Remove and get the last elements in a list.
1186
1292
  #
1187
1293
  # @param [String] key
1188
- # @return [String]
1189
- 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)
1190
1297
  synchronize do |client|
1191
- client.call([:rpop, key])
1298
+ command = [:rpop, key]
1299
+ command << count if count
1300
+ client.call(command)
1192
1301
  end
1193
1302
  end
1194
1303
 
@@ -1470,6 +1579,19 @@ class Redis
1470
1579
  end
1471
1580
  end
1472
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
+
1473
1595
  # Get all the members in a set.
1474
1596
  #
1475
1597
  # @param [String] key
@@ -1574,6 +1696,10 @@ class Redis
1574
1696
  # add elements)
1575
1697
  # - `:nx => true`: Don't update already existing elements (always
1576
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
1577
1703
  # - `:ch => true`: Modify the return value from the number of new
1578
1704
  # elements added, to the total number of elements changed (CH is an
1579
1705
  # abbreviation of changed); changed elements are new elements added
@@ -1588,10 +1714,12 @@ class Redis
1588
1714
  # pairs that were **added** to the sorted set.
1589
1715
  # - `Float` when option :incr is specified, holding the score of the member
1590
1716
  # after incrementing it.
1591
- 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)
1592
1718
  command = [:zadd, key]
1593
1719
  command << "NX" if nx
1594
1720
  command << "XX" if xx
1721
+ command << "LT" if lt
1722
+ command << "GT" if gt
1595
1723
  command << "CH" if ch
1596
1724
  command << "INCR" if incr
1597
1725
 
@@ -1754,6 +1882,63 @@ class Redis
1754
1882
  end
1755
1883
  end
1756
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
+
1757
1942
  # Return a range of members in a sorted set, by index.
1758
1943
  #
1759
1944
  # @example Retrieve all members from a sorted set
@@ -2056,6 +2241,45 @@ class Redis
2056
2241
  end
2057
2242
  end
2058
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
+
2059
2283
  # Intersect multiple sorted sets and store the resulting sorted set in a new
2060
2284
  # key.
2061
2285
  #
@@ -3232,6 +3456,38 @@ class Redis
3232
3456
  synchronize { |client| client.call(args, &blk) }
3233
3457
  end
3234
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
+
3235
3491
  # Fetches not acknowledging pending entries
3236
3492
  #
3237
3493
  # @example With key and group
@@ -3438,10 +3694,24 @@ class Redis
3438
3694
 
3439
3695
  HashifyStreamEntries = lambda { |reply|
3440
3696
  reply.compact.map do |entry_id, values|
3441
- [entry_id, values.each_slice(2).to_h]
3697
+ [entry_id, values&.each_slice(2)&.to_h]
3442
3698
  end
3443
3699
  }
3444
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
+
3445
3715
  HashifyStreamPendings = lambda { |reply|
3446
3716
  {
3447
3717
  'size' => reply[0],
@@ -3541,6 +3811,21 @@ class Redis
3541
3811
  end
3542
3812
  end
3543
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
3544
3829
  end
3545
3830
 
3546
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.3.0
4
+ version: 4.5.1
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: 2021-06-11 00:00:00.000000000 Z
19
+ date: 2021-10-15 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.3.0
105
+ documentation_uri: https://www.rubydoc.info/gems/redis/4.5.1
106
106
  homepage_uri: https://github.com/redis/redis-rb
107
- source_code_uri: https://github.com/redis/redis-rb/tree/v4.3.0
107
+ source_code_uri: https://github.com/redis/redis-rb/tree/v4.5.1
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
  - - ">="