redis 4.1.4 → 4.5.1

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.
data/lib/redis.rb CHANGED
@@ -4,12 +4,28 @@ require "monitor"
4
4
  require_relative "redis/errors"
5
5
 
6
6
  class Redis
7
- def self.current
8
- @current ||= Redis.new
7
+ @exists_returns_integer = true
8
+
9
+ class << self
10
+ attr_reader :exists_returns_integer
11
+
12
+ def exists_returns_integer=(value)
13
+ unless value
14
+ message = "`Redis#exists(key)` will return an Integer by default in redis-rb 4.3. The option to explicitly " \
15
+ "disable this behaviour via `Redis.exists_returns_integer` will be removed in 5.0. You should use " \
16
+ "`exists?` instead."
17
+
18
+ ::Kernel.warn(message)
19
+ end
20
+
21
+ @exists_returns_integer = value
22
+ end
23
+
24
+ attr_writer :current
9
25
  end
10
26
 
11
- def self.current=(redis)
12
- @current = redis
27
+ def self.current
28
+ @current ||= Redis.new
13
29
  end
14
30
 
15
31
  include MonitorMixin
@@ -17,17 +33,22 @@ class Redis
17
33
  # Create a new client instance
18
34
  #
19
35
  # @param [Hash] options
20
- # @option options [String] :url (value of the environment variable REDIS_URL) a Redis URL, for a TCP connection: `redis://:[password]@[hostname]:[port]/[db]` (password, port and database are optional), for a unix socket connection: `unix://[path to Redis socket]`. This overrides all other options.
36
+ # @option options [String] :url (value of the environment variable REDIS_URL) a Redis URL, for a TCP connection:
37
+ # `redis://:[password]@[hostname]:[port]/[db]` (password, port and database are optional), for a unix socket
38
+ # connection: `unix://[path to Redis socket]`. This overrides all other options.
21
39
  # @option options [String] :host ("127.0.0.1") server hostname
22
40
  # @option options [Integer] :port (6379) server port
23
41
  # @option options [String] :path path to server socket (overrides host and port)
24
42
  # @option options [Float] :timeout (5.0) timeout in seconds
25
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
26
45
  # @option options [String] :password Password to authenticate against server
27
46
  # @option options [Integer] :db (0) Database to select after initial connect
28
47
  # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`, `:synchrony`
29
- # @option options [String] :id ID for the client connection, assigns name to current connection by sending `CLIENT SETNAME`
30
- # @option options [Hash, Integer] :tcp_keepalive Keepalive values, if Integer `intvl` and `probe` are calculated based on the value, if Hash `time`, `intvl` and `probes` can be specified as a Integer
48
+ # @option options [String] :id ID for the client connection, assigns name to current connection by sending
49
+ # `CLIENT SETNAME`
50
+ # @option options [Hash, Integer] :tcp_keepalive Keepalive values, if Integer `intvl` and `probe` are calculated
51
+ # based on the value, if Hash `time`, `intvl` and `probes` can be specified as a Integer
31
52
  # @option options [Integer] :reconnect_attempts Number of attempts trying to connect
32
53
  # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
33
54
  # @option options [Array] :sentinels List of sentinels to contact
@@ -52,7 +73,7 @@ class Redis
52
73
  end
53
74
 
54
75
  # Run code with the client reconnecting
55
- def with_reconnect(val=true, &blk)
76
+ def with_reconnect(val = true, &blk)
56
77
  synchronize do |client|
57
78
  client.with_reconnect(val, &blk)
58
79
  end
@@ -125,12 +146,13 @@ class Redis
125
146
 
126
147
  # Authenticate to the server.
127
148
  #
128
- # @param [String] password must match the password specified in the
129
- # `requirepass` directive in the configuration file
149
+ # @param [Array<String>] args includes both username and password
150
+ # or only password
130
151
  # @return [String] `OK`
131
- def auth(password)
152
+ # @see https://redis.io/commands/auth AUTH command
153
+ def auth(*args)
132
154
  synchronize do |client|
133
- client.call([:auth, password])
155
+ client.call([:auth, *args])
134
156
  end
135
157
  end
136
158
 
@@ -205,7 +227,7 @@ class Redis
205
227
  def config(action, *args)
206
228
  synchronize do |client|
207
229
  client.call([:config, action] + args) do |reply|
208
- if reply.kind_of?(Array) && action == :get
230
+ if reply.is_a?(Array) && action == :get
209
231
  Hashify.call(reply)
210
232
  else
211
233
  reply
@@ -256,7 +278,7 @@ class Redis
256
278
  def flushall(options = nil)
257
279
  synchronize do |client|
258
280
  if options && options[:async]
259
- client.call([:flushall, :async])
281
+ client.call(%i[flushall async])
260
282
  else
261
283
  client.call([:flushall])
262
284
  end
@@ -271,7 +293,7 @@ class Redis
271
293
  def flushdb(options = nil)
272
294
  synchronize do |client|
273
295
  if options && options[:async]
274
- client.call([:flushdb, :async])
296
+ client.call(%i[flushdb async])
275
297
  else
276
298
  client.call([:flushdb])
277
299
  end
@@ -285,7 +307,7 @@ class Redis
285
307
  def info(cmd = nil)
286
308
  synchronize do |client|
287
309
  client.call([:info, cmd].compact) do |reply|
288
- if reply.kind_of?(String)
310
+ if reply.is_a?(String)
289
311
  reply = HashifyInfo.call(reply)
290
312
 
291
313
  if cmd && cmd.to_s == "commandstats"
@@ -358,7 +380,7 @@ class Redis
358
380
  # @param [String] subcommand e.g. `get`, `len`, `reset`
359
381
  # @param [Integer] length maximum number of entries to return
360
382
  # @return [Array<String>, Integer, String] depends on subcommand
361
- def slowlog(subcommand, length=nil)
383
+ def slowlog(subcommand, length = nil)
362
384
  synchronize do |client|
363
385
  args = [:slowlog, subcommand]
364
386
  args << length if length
@@ -383,7 +405,7 @@ class Redis
383
405
  def time
384
406
  synchronize do |client|
385
407
  client.call([:time]) do |reply|
386
- reply.map(&:to_i) if reply
408
+ reply&.map(&:to_i)
387
409
  end
388
410
  end
389
411
  end
@@ -496,9 +518,9 @@ class Redis
496
518
  # - `:replace => Boolean`: if false, raises an error if key already exists
497
519
  # @raise [Redis::CommandError]
498
520
  # @return [String] `"OK"`
499
- def restore(key, ttl, serialized_value, options = {})
521
+ def restore(key, ttl, serialized_value, replace: nil)
500
522
  args = [:restore, key, ttl, serialized_value]
501
- args << 'REPLACE' if options[:replace]
523
+ args << 'REPLACE' if replace
502
524
 
503
525
  synchronize do |client|
504
526
  client.call(args)
@@ -535,6 +557,9 @@ class Redis
535
557
  # @param [String, Array<String>] keys
536
558
  # @return [Integer] number of keys that were deleted
537
559
  def del(*keys)
560
+ keys.flatten!(1)
561
+ return 0 if keys.empty?
562
+
538
563
  synchronize do |client|
539
564
  client.call([:del] + keys)
540
565
  end
@@ -550,13 +575,43 @@ class Redis
550
575
  end
551
576
  end
552
577
 
553
- # Determine if a key exists.
578
+ # Determine how many of the keys exists.
554
579
  #
555
- # @param [String] key
580
+ # @param [String, Array<String>] keys
581
+ # @return [Integer]
582
+ def exists(*keys)
583
+ if !Redis.exists_returns_integer && keys.size == 1
584
+ if Redis.exists_returns_integer.nil?
585
+ message = "`Redis#exists(key)` will return an Integer in redis-rb 4.3. `exists?` returns a boolean, you " \
586
+ "should use it instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = " \
587
+ "true. To disable this message and keep the current (boolean) behaviour of 'exists' you can set " \
588
+ "`Redis.exists_returns_integer = false`, but this option will be removed in 5.0. " \
589
+ "(#{::Kernel.caller(1, 1).first})\n"
590
+
591
+ ::Kernel.warn(message)
592
+ end
593
+
594
+ exists?(*keys)
595
+ else
596
+ _exists(*keys)
597
+ end
598
+ end
599
+
600
+ def _exists(*keys)
601
+ synchronize do |client|
602
+ client.call([:exists, *keys])
603
+ end
604
+ end
605
+
606
+ # Determine if any of the keys exists.
607
+ #
608
+ # @param [String, Array<String>] keys
556
609
  # @return [Boolean]
557
- def exists(key)
610
+ def exists?(*keys)
558
611
  synchronize do |client|
559
- client.call([:exists, key], &Boolify)
612
+ client.call([:exists, *keys]) do |value|
613
+ value > 0
614
+ end
560
615
  end
561
616
  end
562
617
 
@@ -567,7 +622,7 @@ class Redis
567
622
  def keys(pattern = "*")
568
623
  synchronize do |client|
569
624
  client.call([:keys, pattern]) do |reply|
570
- if reply.kind_of?(String)
625
+ if reply.is_a?(String)
571
626
  reply.split(" ")
572
627
  else
573
628
  reply
@@ -663,30 +718,27 @@ class Redis
663
718
  # elements where every element is an array with the result for every
664
719
  # element specified in `:get`
665
720
  # - when `:store` is specified, the number of elements in the stored result
666
- def sort(key, options = {})
667
- args = []
668
-
669
- by = options[:by]
670
- args.concat(["BY", by]) if by
721
+ def sort(key, by: nil, limit: nil, get: nil, order: nil, store: nil)
722
+ args = [:sort, key]
723
+ args << "BY" << by if by
671
724
 
672
- limit = options[:limit]
673
- args.concat(["LIMIT"] + limit) if limit
725
+ if limit
726
+ args << "LIMIT"
727
+ args.concat(limit)
728
+ end
674
729
 
675
- get = Array(options[:get])
676
- args.concat(["GET"].product(get).flatten) unless get.empty?
730
+ get = Array(get)
731
+ get.each do |item|
732
+ args << "GET" << item
733
+ end
677
734
 
678
- order = options[:order]
679
735
  args.concat(order.split(" ")) if order
680
-
681
- store = options[:store]
682
- args.concat(["STORE", store]) if store
736
+ args << "STORE" << store if store
683
737
 
684
738
  synchronize do |client|
685
- client.call([:sort, key] + args) do |reply|
739
+ client.call(args) do |reply|
686
740
  if get.size > 1 && !store
687
- if reply
688
- reply.each_slice(get.size).to_a
689
- end
741
+ reply.each_slice(get.size).to_a if reply
690
742
  else
691
743
  reply
692
744
  end
@@ -784,29 +836,29 @@ class Redis
784
836
  # @param [Hash] options
785
837
  # - `:ex => Integer`: Set the specified expire time, in seconds.
786
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.
787
841
  # - `:nx => true`: Only set the key if it does not already exist.
788
842
  # - `:xx => true`: Only set the key if it already exist.
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.
789
845
  # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
790
- def set(key, value, options = {})
791
- args = []
792
-
793
- ex = options[:ex]
794
- args.concat(["EX", ex]) if ex
795
-
796
- px = options[:px]
797
- args.concat(["PX", px]) if px
798
-
799
- nx = options[:nx]
800
- args.concat(["NX"]) if nx
801
-
802
- xx = options[:xx]
803
- args.concat(["XX"]) if xx
846
+ def set(key, value, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil, keepttl: nil, get: nil)
847
+ args = [:set, key, value.to_s]
848
+ args << "EX" << ex if ex
849
+ args << "PX" << px if px
850
+ args << "EXAT" << exat if exat
851
+ args << "PXAT" << pxat if pxat
852
+ args << "NX" if nx
853
+ args << "XX" if xx
854
+ args << "KEEPTTL" if keepttl
855
+ args << "GET" if get
804
856
 
805
857
  synchronize do |client|
806
858
  if nx || xx
807
- client.call([:set, key, value.to_s] + args, &BoolifySet)
859
+ client.call(args, &BoolifySet)
808
860
  else
809
- client.call([:set, key, value.to_s] + args)
861
+ client.call(args)
810
862
  end
811
863
  end
812
864
  end
@@ -888,7 +940,7 @@ class Redis
888
940
  # @see #mapped_msetnx
889
941
  def msetnx(*args)
890
942
  synchronize do |client|
891
- client.call([:msetnx] + args, &Boolify)
943
+ client.call([:msetnx, *args], &Boolify)
892
944
  end
893
945
  end
894
946
 
@@ -928,7 +980,7 @@ class Redis
928
980
  # @see #mapped_mget
929
981
  def mget(*keys, &blk)
930
982
  synchronize do |client|
931
- client.call([:mget] + keys, &blk)
983
+ client.call([:mget, *keys], &blk)
932
984
  end
933
985
  end
934
986
 
@@ -944,7 +996,7 @@ class Redis
944
996
  # @see #mget
945
997
  def mapped_mget(*keys)
946
998
  mget(*keys) do |reply|
947
- if reply.kind_of?(Array)
999
+ if reply.is_a?(Array)
948
1000
  Hash[keys.zip(reply)]
949
1001
  else
950
1002
  reply
@@ -1031,7 +1083,7 @@ class Redis
1031
1083
  # @return [Integer] the length of the string stored in `destkey`
1032
1084
  def bitop(operation, destkey, *keys)
1033
1085
  synchronize do |client|
1034
- client.call([:bitop, operation, destkey] + keys)
1086
+ client.call([:bitop, operation, destkey, *keys])
1035
1087
  end
1036
1088
  end
1037
1089
 
@@ -1043,10 +1095,8 @@ class Redis
1043
1095
  # @param [Integer] stop stop index
1044
1096
  # @return [Integer] the position of the first 1/0 bit.
1045
1097
  # -1 if looking for 1 and it is not found or start and stop are given.
1046
- def bitpos(key, bit, start=nil, stop=nil)
1047
- if stop and not start
1048
- raise(ArgumentError, 'stop parameter specified without start parameter')
1049
- end
1098
+ def bitpos(key, bit, start = nil, stop = nil)
1099
+ raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
1050
1100
 
1051
1101
  synchronize do |client|
1052
1102
  command = [:bitpos, key, bit]
@@ -1068,6 +1118,45 @@ class Redis
1068
1118
  end
1069
1119
  end
1070
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
+
1071
1160
  # Get the length of the value stored in a key.
1072
1161
  #
1073
1162
  # @param [String] key
@@ -1089,6 +1178,59 @@ class Redis
1089
1178
  end
1090
1179
  end
1091
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
+
1092
1234
  # Prepend one or more values to a list, creating the list if it doesn't exist
1093
1235
  #
1094
1236
  # @param [String] key
@@ -1133,23 +1275,29 @@ class Redis
1133
1275
  end
1134
1276
  end
1135
1277
 
1136
- # Remove and get the first element in a list.
1278
+ # Remove and get the first elements in a list.
1137
1279
  #
1138
1280
  # @param [String] key
1139
- # @return [String]
1140
- 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)
1141
1284
  synchronize do |client|
1142
- client.call([:lpop, key])
1285
+ command = [:lpop, key]
1286
+ command << count if count
1287
+ client.call(command)
1143
1288
  end
1144
1289
  end
1145
1290
 
1146
- # Remove and get the last element in a list.
1291
+ # Remove and get the last elements in a list.
1147
1292
  #
1148
1293
  # @param [String] key
1149
- # @return [String]
1150
- 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)
1151
1297
  synchronize do |client|
1152
- client.call([:rpop, key])
1298
+ command = [:rpop, key]
1299
+ command << count if count
1300
+ client.call(command)
1153
1301
  end
1154
1302
  end
1155
1303
 
@@ -1240,15 +1388,7 @@ class Redis
1240
1388
  # @return [nil, String]
1241
1389
  # - `nil` when the operation timed out
1242
1390
  # - the element was popped and pushed otherwise
1243
- def brpoplpush(source, destination, options = {})
1244
- case options
1245
- when Integer
1246
- # Issue deprecation notice in obnoxious mode...
1247
- options = { :timeout => options }
1248
- end
1249
-
1250
- timeout = options[:timeout] || 0
1251
-
1391
+ def brpoplpush(source, destination, deprecated_timeout = 0, timeout: deprecated_timeout)
1252
1392
  synchronize do |client|
1253
1393
  command = [:brpoplpush, source, destination, timeout]
1254
1394
  timeout += client.timeout if timeout > 0
@@ -1439,6 +1579,19 @@ class Redis
1439
1579
  end
1440
1580
  end
1441
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
+
1442
1595
  # Get all the members in a set.
1443
1596
  #
1444
1597
  # @param [String] key
@@ -1455,7 +1608,7 @@ class Redis
1455
1608
  # @return [Array<String>] members in the difference
1456
1609
  def sdiff(*keys)
1457
1610
  synchronize do |client|
1458
- client.call([:sdiff] + keys)
1611
+ client.call([:sdiff, *keys])
1459
1612
  end
1460
1613
  end
1461
1614
 
@@ -1466,7 +1619,7 @@ class Redis
1466
1619
  # @return [Integer] number of elements in the resulting set
1467
1620
  def sdiffstore(destination, *keys)
1468
1621
  synchronize do |client|
1469
- client.call([:sdiffstore, destination] + keys)
1622
+ client.call([:sdiffstore, destination, *keys])
1470
1623
  end
1471
1624
  end
1472
1625
 
@@ -1476,7 +1629,7 @@ class Redis
1476
1629
  # @return [Array<String>] members in the intersection
1477
1630
  def sinter(*keys)
1478
1631
  synchronize do |client|
1479
- client.call([:sinter] + keys)
1632
+ client.call([:sinter, *keys])
1480
1633
  end
1481
1634
  end
1482
1635
 
@@ -1487,7 +1640,7 @@ class Redis
1487
1640
  # @return [Integer] number of elements in the resulting set
1488
1641
  def sinterstore(destination, *keys)
1489
1642
  synchronize do |client|
1490
- client.call([:sinterstore, destination] + keys)
1643
+ client.call([:sinterstore, destination, *keys])
1491
1644
  end
1492
1645
  end
1493
1646
 
@@ -1497,7 +1650,7 @@ class Redis
1497
1650
  # @return [Array<String>] members in the union
1498
1651
  def sunion(*keys)
1499
1652
  synchronize do |client|
1500
- client.call([:sunion] + keys)
1653
+ client.call([:sunion, *keys])
1501
1654
  end
1502
1655
  end
1503
1656
 
@@ -1508,7 +1661,7 @@ class Redis
1508
1661
  # @return [Integer] number of elements in the resulting set
1509
1662
  def sunionstore(destination, *keys)
1510
1663
  synchronize do |client|
1511
- client.call([:sunionstore, destination] + keys)
1664
+ client.call([:sunionstore, destination, *keys])
1512
1665
  end
1513
1666
  end
1514
1667
 
@@ -1543,6 +1696,10 @@ class Redis
1543
1696
  # add elements)
1544
1697
  # - `:nx => true`: Don't update already existing elements (always
1545
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
1546
1703
  # - `:ch => true`: Modify the return value from the number of new
1547
1704
  # elements added, to the total number of elements changed (CH is an
1548
1705
  # abbreviation of changed); changed elements are new elements added
@@ -1557,31 +1714,22 @@ class Redis
1557
1714
  # pairs that were **added** to the sorted set.
1558
1715
  # - `Float` when option :incr is specified, holding the score of the member
1559
1716
  # after incrementing it.
1560
- def zadd(key, *args) #, options
1561
- zadd_options = []
1562
- if args.last.is_a?(Hash)
1563
- options = args.pop
1564
-
1565
- nx = options[:nx]
1566
- zadd_options << "NX" if nx
1567
-
1568
- xx = options[:xx]
1569
- zadd_options << "XX" if xx
1570
-
1571
- ch = options[:ch]
1572
- zadd_options << "CH" if ch
1573
-
1574
- incr = options[:incr]
1575
- zadd_options << "INCR" if incr
1576
- end
1717
+ def zadd(key, *args, nx: nil, xx: nil, lt: nil, gt: nil, ch: nil, incr: nil)
1718
+ command = [:zadd, key]
1719
+ command << "NX" if nx
1720
+ command << "XX" if xx
1721
+ command << "LT" if lt
1722
+ command << "GT" if gt
1723
+ command << "CH" if ch
1724
+ command << "INCR" if incr
1577
1725
 
1578
1726
  synchronize do |client|
1579
1727
  if args.size == 1 && args[0].is_a?(Array)
1580
1728
  # Variadic: return float if INCR, integer if !INCR
1581
- client.call([:zadd, key] + zadd_options + args[0], &(incr ? Floatify : nil))
1729
+ client.call(command + args[0], &(incr ? Floatify : nil))
1582
1730
  elsif args.size == 2
1583
1731
  # Single pair: return float if INCR, boolean if !INCR
1584
- client.call([:zadd, key] + zadd_options + args, &(incr ? Floatify : Boolify))
1732
+ client.call(command + args, &(incr ? Floatify : Boolify))
1585
1733
  else
1586
1734
  raise ArgumentError, "wrong number of arguments"
1587
1735
  end
@@ -1734,6 +1882,63 @@ class Redis
1734
1882
  end
1735
1883
  end
1736
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
+
1737
1942
  # Return a range of members in a sorted set, by index.
1738
1943
  #
1739
1944
  # @example Retrieve all members from a sorted set
@@ -1752,10 +1957,8 @@ class Redis
1752
1957
  # @return [Array<String>, Array<[String, Float]>]
1753
1958
  # - when `:with_scores` is not specified, an array of members
1754
1959
  # - when `:with_scores` is specified, an array with `[member, score]` pairs
1755
- def zrange(key, start, stop, options = {})
1756
- args = []
1757
-
1758
- with_scores = options[:with_scores] || options[:withscores]
1960
+ def zrange(key, start, stop, withscores: false, with_scores: withscores)
1961
+ args = [:zrange, key, start, stop]
1759
1962
 
1760
1963
  if with_scores
1761
1964
  args << "WITHSCORES"
@@ -1763,7 +1966,7 @@ class Redis
1763
1966
  end
1764
1967
 
1765
1968
  synchronize do |client|
1766
- client.call([:zrange, key, start, stop] + args, &block)
1969
+ client.call(args, &block)
1767
1970
  end
1768
1971
  end
1769
1972
 
@@ -1778,10 +1981,8 @@ class Redis
1778
1981
  # # => [["b", 64.0], ["a", 32.0]]
1779
1982
  #
1780
1983
  # @see #zrange
1781
- def zrevrange(key, start, stop, options = {})
1782
- args = []
1783
-
1784
- with_scores = options[:with_scores] || options[:withscores]
1984
+ def zrevrange(key, start, stop, withscores: false, with_scores: withscores)
1985
+ args = [:zrevrange, key, start, stop]
1785
1986
 
1786
1987
  if with_scores
1787
1988
  args << "WITHSCORES"
@@ -1789,7 +1990,7 @@ class Redis
1789
1990
  end
1790
1991
 
1791
1992
  synchronize do |client|
1792
- client.call([:zrevrange, key, start, stop] + args, &block)
1993
+ client.call(args, &block)
1793
1994
  end
1794
1995
  end
1795
1996
 
@@ -1880,14 +2081,16 @@ class Redis
1880
2081
  # `count` members
1881
2082
  #
1882
2083
  # @return [Array<String>, Array<[String, Float]>]
1883
- def zrangebylex(key, min, max, options = {})
1884
- args = []
2084
+ def zrangebylex(key, min, max, limit: nil)
2085
+ args = [:zrangebylex, key, min, max]
1885
2086
 
1886
- limit = options[:limit]
1887
- args.concat(["LIMIT"] + limit) if limit
2087
+ if limit
2088
+ args << "LIMIT"
2089
+ args.concat(limit)
2090
+ end
1888
2091
 
1889
2092
  synchronize do |client|
1890
- client.call([:zrangebylex, key, min, max] + args)
2093
+ client.call(args)
1891
2094
  end
1892
2095
  end
1893
2096
 
@@ -1902,14 +2105,16 @@ class Redis
1902
2105
  # # => ["abbygail", "abby"]
1903
2106
  #
1904
2107
  # @see #zrangebylex
1905
- def zrevrangebylex(key, max, min, options = {})
1906
- args = []
2108
+ def zrevrangebylex(key, max, min, limit: nil)
2109
+ args = [:zrevrangebylex, key, max, min]
1907
2110
 
1908
- limit = options[:limit]
1909
- args.concat(["LIMIT"] + limit) if limit
2111
+ if limit
2112
+ args << "LIMIT"
2113
+ args.concat(limit)
2114
+ end
1910
2115
 
1911
2116
  synchronize do |client|
1912
- client.call([:zrevrangebylex, key, max, min] + args)
2117
+ client.call(args)
1913
2118
  end
1914
2119
  end
1915
2120
 
@@ -1940,21 +2145,21 @@ class Redis
1940
2145
  # @return [Array<String>, Array<[String, Float]>]
1941
2146
  # - when `:with_scores` is not specified, an array of members
1942
2147
  # - when `:with_scores` is specified, an array with `[member, score]` pairs
1943
- def zrangebyscore(key, min, max, options = {})
1944
- args = []
1945
-
1946
- with_scores = options[:with_scores] || options[:withscores]
2148
+ def zrangebyscore(key, min, max, withscores: false, with_scores: withscores, limit: nil)
2149
+ args = [:zrangebyscore, key, min, max]
1947
2150
 
1948
2151
  if with_scores
1949
2152
  args << "WITHSCORES"
1950
2153
  block = FloatifyPairs
1951
2154
  end
1952
2155
 
1953
- limit = options[:limit]
1954
- args.concat(["LIMIT"] + limit) if limit
2156
+ if limit
2157
+ args << "LIMIT"
2158
+ args.concat(limit)
2159
+ end
1955
2160
 
1956
2161
  synchronize do |client|
1957
- client.call([:zrangebyscore, key, min, max] + args, &block)
2162
+ client.call(args, &block)
1958
2163
  end
1959
2164
  end
1960
2165
 
@@ -1972,21 +2177,21 @@ class Redis
1972
2177
  # # => [["b", 64.0], ["a", 32.0]]
1973
2178
  #
1974
2179
  # @see #zrangebyscore
1975
- def zrevrangebyscore(key, max, min, options = {})
1976
- args = []
1977
-
1978
- with_scores = options[:with_scores] || options[:withscores]
2180
+ def zrevrangebyscore(key, max, min, withscores: false, with_scores: withscores, limit: nil)
2181
+ args = [:zrevrangebyscore, key, max, min]
1979
2182
 
1980
2183
  if with_scores
1981
- args << ["WITHSCORES"]
2184
+ args << "WITHSCORES"
1982
2185
  block = FloatifyPairs
1983
2186
  end
1984
2187
 
1985
- limit = options[:limit]
1986
- args.concat(["LIMIT"] + limit) if limit
2188
+ if limit
2189
+ args << "LIMIT"
2190
+ args.concat(limit)
2191
+ end
1987
2192
 
1988
2193
  synchronize do |client|
1989
- client.call([:zrevrangebyscore, key, max, min] + args, &block)
2194
+ client.call(args, &block)
1990
2195
  end
1991
2196
  end
1992
2197
 
@@ -2036,6 +2241,45 @@ class Redis
2036
2241
  end
2037
2242
  end
2038
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
+
2039
2283
  # Intersect multiple sorted sets and store the resulting sorted set in a new
2040
2284
  # key.
2041
2285
  #
@@ -2050,17 +2294,18 @@ class Redis
2050
2294
  # sorted sets
2051
2295
  # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
2052
2296
  # @return [Integer] number of elements in the resulting sorted set
2053
- def zinterstore(destination, keys, options = {})
2054
- args = []
2297
+ def zinterstore(destination, keys, weights: nil, aggregate: nil)
2298
+ args = [:zinterstore, destination, keys.size, *keys]
2055
2299
 
2056
- weights = options[:weights]
2057
- args.concat(["WEIGHTS"] + weights) if weights
2300
+ if weights
2301
+ args << "WEIGHTS"
2302
+ args.concat(weights)
2303
+ end
2058
2304
 
2059
- aggregate = options[:aggregate]
2060
- args.concat(["AGGREGATE", aggregate]) if aggregate
2305
+ args << "AGGREGATE" << aggregate if aggregate
2061
2306
 
2062
2307
  synchronize do |client|
2063
- client.call([:zinterstore, destination, keys.size] + keys + args)
2308
+ client.call(args)
2064
2309
  end
2065
2310
  end
2066
2311
 
@@ -2077,17 +2322,18 @@ class Redis
2077
2322
  # sorted sets
2078
2323
  # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
2079
2324
  # @return [Integer] number of elements in the resulting sorted set
2080
- def zunionstore(destination, keys, options = {})
2081
- args = []
2325
+ def zunionstore(destination, keys, weights: nil, aggregate: nil)
2326
+ args = [:zunionstore, destination, keys.size, *keys]
2082
2327
 
2083
- weights = options[:weights]
2084
- args.concat(["WEIGHTS"] + weights) if weights
2328
+ if weights
2329
+ args << "WEIGHTS"
2330
+ args.concat(weights)
2331
+ end
2085
2332
 
2086
- aggregate = options[:aggregate]
2087
- args.concat(["AGGREGATE", aggregate]) if aggregate
2333
+ args << "AGGREGATE" << aggregate if aggregate
2088
2334
 
2089
2335
  synchronize do |client|
2090
- client.call([:zunionstore, destination, keys.size] + keys + args)
2336
+ client.call(args)
2091
2337
  end
2092
2338
  end
2093
2339
 
@@ -2101,15 +2347,20 @@ class Redis
2101
2347
  end
2102
2348
  end
2103
2349
 
2104
- # Set the string value of a hash field.
2350
+ # Set one or more hash values.
2351
+ #
2352
+ # @example
2353
+ # redis.hset("hash", "f1", "v1", "f2", "v2") # => 2
2354
+ # redis.hset("hash", { "f1" => "v1", "f2" => "v2" }) # => 2
2105
2355
  #
2106
2356
  # @param [String] key
2107
- # @param [String] field
2108
- # @param [String] value
2109
- # @return [Boolean] whether or not the field was **added** to the hash
2110
- def hset(key, field, value)
2357
+ # @param [Array<String> | Hash<String, String>] attrs array or hash of fields and values
2358
+ # @return [Integer] The number of fields that were added to the hash
2359
+ def hset(key, *attrs)
2360
+ attrs = attrs.first.flatten if attrs.size == 1 && attrs.first.is_a?(Hash)
2361
+
2111
2362
  synchronize do |client|
2112
- client.call([:hset, key, field, value], &Boolify)
2363
+ client.call([:hset, key, *attrs])
2113
2364
  end
2114
2365
  end
2115
2366
 
@@ -2198,7 +2449,7 @@ class Redis
2198
2449
  # @see #hmget
2199
2450
  def mapped_hmget(key, *fields)
2200
2451
  hmget(key, *fields) do |reply|
2201
- if reply.kind_of?(Array)
2452
+ if reply.is_a?(Array)
2202
2453
  Hash[fields.zip(reply)]
2203
2454
  else
2204
2455
  reply
@@ -2291,20 +2542,21 @@ class Redis
2291
2542
 
2292
2543
  def subscribed?
2293
2544
  synchronize do |client|
2294
- client.kind_of? SubscribedClient
2545
+ client.is_a? SubscribedClient
2295
2546
  end
2296
2547
  end
2297
2548
 
2298
2549
  # Listen for messages published to the given channels.
2299
2550
  def subscribe(*channels, &block)
2300
- synchronize do |client|
2551
+ synchronize do |_client|
2301
2552
  _subscription(:subscribe, 0, channels, block)
2302
2553
  end
2303
2554
  end
2304
2555
 
2305
- # Listen for messages published to the given channels. Throw a timeout error if there is no messages for a timeout period.
2556
+ # Listen for messages published to the given channels. Throw a timeout error
2557
+ # if there is no messages for a timeout period.
2306
2558
  def subscribe_with_timeout(timeout, *channels, &block)
2307
- synchronize do |client|
2559
+ synchronize do |_client|
2308
2560
  _subscription(:subscribe_with_timeout, timeout, channels, block)
2309
2561
  end
2310
2562
  end
@@ -2312,21 +2564,23 @@ class Redis
2312
2564
  # Stop listening for messages posted to the given channels.
2313
2565
  def unsubscribe(*channels)
2314
2566
  synchronize do |client|
2315
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2567
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2568
+
2316
2569
  client.unsubscribe(*channels)
2317
2570
  end
2318
2571
  end
2319
2572
 
2320
2573
  # Listen for messages published to channels matching the given patterns.
2321
2574
  def psubscribe(*channels, &block)
2322
- synchronize do |client|
2575
+ synchronize do |_client|
2323
2576
  _subscription(:psubscribe, 0, channels, block)
2324
2577
  end
2325
2578
  end
2326
2579
 
2327
- # Listen for messages published to channels matching the given patterns. Throw a timeout error if there is no messages for a timeout period.
2580
+ # Listen for messages published to channels matching the given patterns.
2581
+ # Throw a timeout error if there is no messages for a timeout period.
2328
2582
  def psubscribe_with_timeout(timeout, *channels, &block)
2329
- synchronize do |client|
2583
+ synchronize do |_client|
2330
2584
  _subscription(:psubscribe_with_timeout, timeout, channels, block)
2331
2585
  end
2332
2586
  end
@@ -2334,7 +2588,8 @@ class Redis
2334
2588
  # Stop listening for messages posted to channels matching the given patterns.
2335
2589
  def punsubscribe(*channels)
2336
2590
  synchronize do |client|
2337
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2591
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2592
+
2338
2593
  client.punsubscribe(*channels)
2339
2594
  end
2340
2595
  end
@@ -2379,7 +2634,7 @@ class Redis
2379
2634
  # @see #multi
2380
2635
  def watch(*keys)
2381
2636
  synchronize do |client|
2382
- res = client.call([:watch] + keys)
2637
+ res = client.call([:watch, *keys])
2383
2638
 
2384
2639
  if block_given?
2385
2640
  begin
@@ -2409,14 +2664,13 @@ class Redis
2409
2664
  end
2410
2665
 
2411
2666
  def pipelined
2412
- synchronize do |client|
2667
+ synchronize do |prior_client|
2413
2668
  begin
2414
- pipeline = Pipeline.new(@client)
2415
- original, @client = @client, pipeline
2669
+ @client = Pipeline.new(prior_client)
2416
2670
  yield(self)
2417
- original.call_pipeline(@client)
2671
+ prior_client.call_pipeline(@client)
2418
2672
  ensure
2419
- @client = original
2673
+ @client = prior_client
2420
2674
  end
2421
2675
  end
2422
2676
  end
@@ -2452,17 +2706,16 @@ class Redis
2452
2706
  # @see #watch
2453
2707
  # @see #unwatch
2454
2708
  def multi
2455
- synchronize do |client|
2709
+ synchronize do |prior_client|
2456
2710
  if !block_given?
2457
- client.call([:multi])
2711
+ prior_client.call([:multi])
2458
2712
  else
2459
2713
  begin
2460
- pipeline = Pipeline::Multi.new(@client)
2461
- original, @client = @client, pipeline
2714
+ @client = Pipeline::Multi.new(prior_client)
2462
2715
  yield(self)
2463
- original.call_pipeline(pipeline)
2716
+ prior_client.call_pipeline(@client)
2464
2717
  ensure
2465
- @client = original
2718
+ @client = prior_client
2466
2719
  end
2467
2720
  end
2468
2721
  end
@@ -2609,18 +2862,13 @@ class Redis
2609
2862
  _eval(:evalsha, args)
2610
2863
  end
2611
2864
 
2612
- def _scan(command, cursor, args, options = {}, &block)
2865
+ def _scan(command, cursor, args, match: nil, count: nil, type: nil, &block)
2613
2866
  # SSCAN/ZSCAN/HSCAN already prepend the key to +args+.
2614
2867
 
2615
2868
  args << cursor
2616
-
2617
- if match = options[:match]
2618
- args.concat(["MATCH", match])
2619
- end
2620
-
2621
- if count = options[:count]
2622
- args.concat(["COUNT", count])
2623
- end
2869
+ args << "MATCH" << match if match
2870
+ args << "COUNT" << count if count
2871
+ args << "TYPE" << type if type
2624
2872
 
2625
2873
  synchronize do |client|
2626
2874
  client.call([command] + args, &block)
@@ -2635,15 +2883,19 @@ class Redis
2635
2883
  # @example Retrieve a batch of keys matching a pattern
2636
2884
  # redis.scan(4, :match => "key:1?")
2637
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"]]
2638
2889
  #
2639
2890
  # @param [String, Integer] cursor the cursor of the iteration
2640
2891
  # @param [Hash] options
2641
2892
  # - `:match => String`: only return keys matching the pattern
2642
2893
  # - `:count => Integer`: return count keys at most per iteration
2894
+ # - `:type => String`: return keys only of the given type
2643
2895
  #
2644
2896
  # @return [String, Array<String>] the next cursor and all found keys
2645
- def scan(cursor, options={})
2646
- _scan(:scan, cursor, [], options)
2897
+ def scan(cursor, **options)
2898
+ _scan(:scan, cursor, [], **options)
2647
2899
  end
2648
2900
 
2649
2901
  # Scan the keyspace
@@ -2655,17 +2907,23 @@ class Redis
2655
2907
  # redis.scan_each(:match => "key:1?") {|key| puts key}
2656
2908
  # # => key:13
2657
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"
2658
2914
  #
2659
2915
  # @param [Hash] options
2660
2916
  # - `:match => String`: only return keys matching the pattern
2661
2917
  # - `:count => Integer`: return count keys at most per iteration
2918
+ # - `:type => String`: return keys only of the given type
2662
2919
  #
2663
2920
  # @return [Enumerator] an enumerator for all found keys
2664
- def scan_each(options={}, &block)
2665
- return to_enum(:scan_each, options) unless block_given?
2921
+ def scan_each(**options, &block)
2922
+ return to_enum(:scan_each, **options) unless block_given?
2923
+
2666
2924
  cursor = 0
2667
2925
  loop do
2668
- cursor, keys = scan(cursor, options)
2926
+ cursor, keys = scan(cursor, **options)
2669
2927
  keys.each(&block)
2670
2928
  break if cursor == "0"
2671
2929
  end
@@ -2682,8 +2940,8 @@ class Redis
2682
2940
  # - `:count => Integer`: return count keys at most per iteration
2683
2941
  #
2684
2942
  # @return [String, Array<[String, String]>] the next cursor and all found keys
2685
- def hscan(key, cursor, options={})
2686
- _scan(:hscan, cursor, [key], options) do |reply|
2943
+ def hscan(key, cursor, **options)
2944
+ _scan(:hscan, cursor, [key], **options) do |reply|
2687
2945
  [reply[0], reply[1].each_slice(2).to_a]
2688
2946
  end
2689
2947
  end
@@ -2699,11 +2957,12 @@ class Redis
2699
2957
  # - `:count => Integer`: return count keys at most per iteration
2700
2958
  #
2701
2959
  # @return [Enumerator] an enumerator for all found keys
2702
- def hscan_each(key, options={}, &block)
2703
- return to_enum(:hscan_each, key, options) unless block_given?
2960
+ def hscan_each(key, **options, &block)
2961
+ return to_enum(:hscan_each, key, **options) unless block_given?
2962
+
2704
2963
  cursor = 0
2705
2964
  loop do
2706
- cursor, values = hscan(key, cursor, options)
2965
+ cursor, values = hscan(key, cursor, **options)
2707
2966
  values.each(&block)
2708
2967
  break if cursor == "0"
2709
2968
  end
@@ -2721,8 +2980,8 @@ class Redis
2721
2980
  #
2722
2981
  # @return [String, Array<[String, Float]>] the next cursor and all found
2723
2982
  # members and scores
2724
- def zscan(key, cursor, options={})
2725
- _scan(:zscan, cursor, [key], options) do |reply|
2983
+ def zscan(key, cursor, **options)
2984
+ _scan(:zscan, cursor, [key], **options) do |reply|
2726
2985
  [reply[0], FloatifyPairs.call(reply[1])]
2727
2986
  end
2728
2987
  end
@@ -2738,11 +2997,12 @@ class Redis
2738
2997
  # - `:count => Integer`: return count keys at most per iteration
2739
2998
  #
2740
2999
  # @return [Enumerator] an enumerator for all found scores and members
2741
- def zscan_each(key, options={}, &block)
2742
- return to_enum(:zscan_each, key, options) unless block_given?
3000
+ def zscan_each(key, **options, &block)
3001
+ return to_enum(:zscan_each, key, **options) unless block_given?
3002
+
2743
3003
  cursor = 0
2744
3004
  loop do
2745
- cursor, values = zscan(key, cursor, options)
3005
+ cursor, values = zscan(key, cursor, **options)
2746
3006
  values.each(&block)
2747
3007
  break if cursor == "0"
2748
3008
  end
@@ -2759,8 +3019,8 @@ class Redis
2759
3019
  # - `:count => Integer`: return count keys at most per iteration
2760
3020
  #
2761
3021
  # @return [String, Array<String>] the next cursor and all found members
2762
- def sscan(key, cursor, options={})
2763
- _scan(:sscan, cursor, [key], options)
3022
+ def sscan(key, cursor, **options)
3023
+ _scan(:sscan, cursor, [key], **options)
2764
3024
  end
2765
3025
 
2766
3026
  # Scan a set
@@ -2774,11 +3034,12 @@ class Redis
2774
3034
  # - `:count => Integer`: return count keys at most per iteration
2775
3035
  #
2776
3036
  # @return [Enumerator] an enumerator for all keys in the set
2777
- def sscan_each(key, options={}, &block)
2778
- return to_enum(:sscan_each, key, options) unless block_given?
3037
+ def sscan_each(key, **options, &block)
3038
+ return to_enum(:sscan_each, key, **options) unless block_given?
3039
+
2779
3040
  cursor = 0
2780
3041
  loop do
2781
- cursor, keys = sscan(key, cursor, options)
3042
+ cursor, keys = sscan(key, cursor, **options)
2782
3043
  keys.each(&block)
2783
3044
  break if cursor == "0"
2784
3045
  end
@@ -2842,12 +3103,12 @@ class Redis
2842
3103
  end
2843
3104
  end
2844
3105
 
2845
-
2846
3106
  # Query a sorted set representing a geospatial index to fetch members matching a
2847
3107
  # given maximum distance from a point
2848
3108
  #
2849
3109
  # @param [Array] args key, longitude, latitude, radius, unit(m|km|ft|mi)
2850
- # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest to the nearest relative to the center
3110
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest
3111
+ # or the farthest to the nearest relative to the center
2851
3112
  # @param [Integer] count limit the results to the first N matching items
2852
3113
  # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2853
3114
  # @return [Array<String>] may be changed with `options`
@@ -2864,7 +3125,8 @@ class Redis
2864
3125
  # given maximum distance from an already existing member
2865
3126
  #
2866
3127
  # @param [Array] args key, member, radius, unit(m|km|ft|mi)
2867
- # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest to the nearest relative to the center
3128
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest
3129
+ # to the nearest relative to the center
2868
3130
  # @param [Integer] count limit the results to the first N matching items
2869
3131
  # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2870
3132
  # @return [Array<String>] may be changed with `options`
@@ -2881,7 +3143,8 @@ class Redis
2881
3143
  #
2882
3144
  # @param [String] key
2883
3145
  # @param [String, Array<String>] member one member or array of members
2884
- # @return [Array<Array<String>, nil>] returns array of elements, where each element is either array of longitude and latitude or nil
3146
+ # @return [Array<Array<String>, nil>] returns array of elements, where each
3147
+ # element is either array of longitude and latitude or nil
2885
3148
  def geopos(key, member)
2886
3149
  synchronize do |client|
2887
3150
  client.call([:geopos, key, member])
@@ -2945,10 +3208,14 @@ class Redis
2945
3208
  # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen or not
2946
3209
  #
2947
3210
  # @return [String] the entry id
2948
- def xadd(key, entry, opts = {})
3211
+ def xadd(key, entry, approximate: nil, maxlen: nil, id: '*')
2949
3212
  args = [:xadd, key]
2950
- args.concat(['MAXLEN', (opts[:approximate] ? '~' : nil), opts[:maxlen]].compact) if opts[:maxlen]
2951
- args << (opts[:id] || '*')
3213
+ if maxlen
3214
+ args << "MAXLEN"
3215
+ args << "~" if approximate
3216
+ args << maxlen
3217
+ end
3218
+ args << id
2952
3219
  args.concat(entry.to_a.flatten)
2953
3220
  synchronize { |client| client.call(args) }
2954
3221
  end
@@ -3003,8 +3270,8 @@ class Redis
3003
3270
  # @param count [Integer] the number of entries as limit
3004
3271
  #
3005
3272
  # @return [Array<Array<String, Hash>>] the ids and entries pairs
3006
- def xrange(key, start = '-', _end = '+', count: nil)
3007
- args = [:xrange, key, start, _end]
3273
+ def xrange(key, start = '-', range_end = '+', count: nil)
3274
+ args = [:xrange, key, start, range_end]
3008
3275
  args.concat(['COUNT', count]) if count
3009
3276
  synchronize { |client| client.call(args, &HashifyStreamEntries) }
3010
3277
  end
@@ -3026,8 +3293,8 @@ class Redis
3026
3293
  # @params count [Integer] the number of entries as limit
3027
3294
  #
3028
3295
  # @return [Array<Array<String, Hash>>] the ids and entries pairs
3029
- def xrevrange(key, _end = '+', start = '-', count: nil)
3030
- args = [:xrevrange, key, _end, start]
3296
+ def xrevrange(key, range_end = '+', start = '-', count: nil)
3297
+ args = [:xrevrange, key, range_end, start]
3031
3298
  args.concat(['COUNT', count]) if count
3032
3299
  synchronize { |client| client.call(args, &HashifyStreamEntries) }
3033
3300
  end
@@ -3119,12 +3386,12 @@ class Redis
3119
3386
  # @option opts [Boolean] :noack whether message loss is acceptable or not
3120
3387
  #
3121
3388
  # @return [Hash{String => Hash{String => Hash}}] the entries
3122
- def xreadgroup(group, consumer, keys, ids, opts = {})
3389
+ def xreadgroup(group, consumer, keys, ids, count: nil, block: nil, noack: nil)
3123
3390
  args = [:xreadgroup, 'GROUP', group, consumer]
3124
- args << 'COUNT' << opts[:count] if opts[:count]
3125
- args << 'BLOCK' << opts[:block].to_i if opts[:block]
3126
- args << 'NOACK' if opts[:noack]
3127
- _xread(args, keys, ids, opts[:block])
3391
+ args << 'COUNT' << count if count
3392
+ args << 'BLOCK' << block.to_i if block
3393
+ args << 'NOACK' if noack
3394
+ _xread(args, keys, ids, block)
3128
3395
  end
3129
3396
 
3130
3397
  # Removes one or multiple entries from the pending entries list of a stream consumer group.
@@ -3189,6 +3456,38 @@ class Redis
3189
3456
  synchronize { |client| client.call(args, &blk) }
3190
3457
  end
3191
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
+
3192
3491
  # Fetches not acknowledging pending entries
3193
3492
  #
3194
3493
  # @example With key and group
@@ -3234,8 +3533,8 @@ class Redis
3234
3533
  when "get-master-addr-by-name"
3235
3534
  reply
3236
3535
  else
3237
- if reply.kind_of?(Array)
3238
- if reply[0].kind_of?(Array)
3536
+ if reply.is_a?(Array)
3537
+ if reply[0].is_a?(Array)
3239
3538
  reply.map(&Hashify)
3240
3539
  else
3241
3540
  Hashify.call(reply)
@@ -3259,12 +3558,17 @@ class Redis
3259
3558
  def cluster(subcommand, *args)
3260
3559
  subcommand = subcommand.to_s.downcase
3261
3560
  block = case subcommand
3262
- when 'slots' then HashifyClusterSlots
3263
- when 'nodes' then HashifyClusterNodes
3264
- when 'slaves' then HashifyClusterSlaves
3265
- when 'info' then HashifyInfo
3266
- else Noop
3267
- end
3561
+ when 'slots'
3562
+ HashifyClusterSlots
3563
+ when 'nodes'
3564
+ HashifyClusterNodes
3565
+ when 'slaves'
3566
+ HashifyClusterSlaves
3567
+ when 'info'
3568
+ HashifyInfo
3569
+ else
3570
+ Noop
3571
+ end
3268
3572
 
3269
3573
  # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
3270
3574
  block = Noop unless @cluster_mode
@@ -3299,21 +3603,21 @@ class Redis
3299
3603
  return @original_client.connection_info if @cluster_mode
3300
3604
 
3301
3605
  {
3302
- host: @original_client.host,
3303
- port: @original_client.port,
3304
- db: @original_client.db,
3305
- id: @original_client.id,
3606
+ host: @original_client.host,
3607
+ port: @original_client.port,
3608
+ db: @original_client.db,
3609
+ id: @original_client.id,
3306
3610
  location: @original_client.location
3307
3611
  }
3308
3612
  end
3309
3613
 
3310
- def method_missing(command, *args)
3614
+ def method_missing(command, *args) # rubocop:disable Style/MissingRespondToMissing
3311
3615
  synchronize do |client|
3312
3616
  client.call([command] + args)
3313
3617
  end
3314
3618
  end
3315
3619
 
3316
- private
3620
+ private
3317
3621
 
3318
3622
  # Commands returning 1 for true and 0 for false may be executed in a pipeline
3319
3623
  # where the method call will return nil. Propagate the nil instead of falsely
@@ -3385,18 +3689,35 @@ private
3385
3689
  end
3386
3690
  }
3387
3691
 
3692
+ EMPTY_STREAM_RESPONSE = [nil].freeze
3693
+ private_constant :EMPTY_STREAM_RESPONSE
3694
+
3388
3695
  HashifyStreamEntries = lambda { |reply|
3389
- reply.map do |entry_id, values|
3390
- [entry_id, values.each_slice(2).to_h]
3696
+ reply.compact.map do |entry_id, values|
3697
+ [entry_id, values&.each_slice(2)&.to_h]
3391
3698
  end
3392
3699
  }
3393
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
+
3394
3715
  HashifyStreamPendings = lambda { |reply|
3395
3716
  {
3396
- 'size' => reply[0],
3717
+ 'size' => reply[0],
3397
3718
  'min_entry_id' => reply[1],
3398
3719
  'max_entry_id' => reply[2],
3399
- 'consumers' => reply[3].nil? ? {} : reply[3].to_h
3720
+ 'consumers' => reply[3].nil? ? {} : reply[3].to_h
3400
3721
  }
3401
3722
  }
3402
3723
 
@@ -3405,8 +3726,8 @@ private
3405
3726
  {
3406
3727
  'entry_id' => arr[0],
3407
3728
  'consumer' => arr[1],
3408
- 'elapsed' => arr[2],
3409
- 'count' => arr[3]
3729
+ 'elapsed' => arr[2],
3730
+ 'count' => arr[3]
3410
3731
  }
3411
3732
  end
3412
3733
  }
@@ -3414,15 +3735,15 @@ private
3414
3735
  HashifyClusterNodeInfo = lambda { |str|
3415
3736
  arr = str.split(' ')
3416
3737
  {
3417
- 'node_id' => arr[0],
3418
- 'ip_port' => arr[1],
3419
- 'flags' => arr[2].split(','),
3738
+ 'node_id' => arr[0],
3739
+ 'ip_port' => arr[1],
3740
+ 'flags' => arr[2].split(','),
3420
3741
  'master_node_id' => arr[3],
3421
- 'ping_sent' => arr[4],
3422
- 'pong_recv' => arr[5],
3423
- 'config_epoch' => arr[6],
3424
- 'link_state' => arr[7],
3425
- 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
3742
+ 'ping_sent' => arr[4],
3743
+ 'pong_recv' => arr[5],
3744
+ 'config_epoch' => arr[6],
3745
+ 'link_state' => arr[7],
3746
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
3426
3747
  }
3427
3748
  }
3428
3749
 
@@ -3433,9 +3754,9 @@ private
3433
3754
  replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
3434
3755
  {
3435
3756
  'start_slot' => first_slot,
3436
- 'end_slot' => last_slot,
3437
- 'master' => master,
3438
- 'replicas' => replicas
3757
+ 'end_slot' => last_slot,
3758
+ 'master' => master,
3759
+ 'replicas' => replicas
3439
3760
  }
3440
3761
  end
3441
3762
  }
@@ -3490,6 +3811,21 @@ private
3490
3811
  end
3491
3812
  end
3492
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
3493
3829
  end
3494
3830
 
3495
3831
  require_relative "redis/version"