redis 4.1.4 → 4.4.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: 83f1f7270db68603d63e86ec43e68348cb5ccb2b4e6759642d89898566bdbaf6
4
- data.tar.gz: 45c5bcc92629ec7d85cdc2b913e7922cd5425f2e6691891efc379aeec73026b3
3
+ metadata.gz: d5ff2ee4b6a6f2b087ac26bf96a3c1769cf42f70ea90008361157f7ef04cdb14
4
+ data.tar.gz: f2c24654294c4fa81a5cff4ddb545bc59e6f85b64ee61273278cb1e286a4edb8
5
5
  SHA512:
6
- metadata.gz: 692dfc5c73c6410492589f38f279976a023f6a2ff13f7b1476806011eb387f41bed784bdeac746de5f4b990b6d22bf297b36dddc7b8e448a842241a389f50796
7
- data.tar.gz: 55a9e305c7563f5dd7d38f50dc7b919967dbb0f6a131ebc5e1569f49f196ab458203b6594394fa9a33ea9e337b741113e781378113783683dd36b87196607b8f
6
+ metadata.gz: 0d88c6621659a178dca04d3ca62f25520f5f1a846b6300f93fb008966551fb4795a3e6c37810f99ea18f77a6292b955fa180ae3e06f8c90df7e6a3ca27087ae1
7
+ data.tar.gz: 1427cc268e867872388f214184d23a5c6062104fcb9d1e50d6026dd9daf6e51b9833866fee9fcf80c7a64e4de705295de8399d8e8ac22fa890bf358bd63ff707
data/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # Unreleased
2
2
 
3
+ # 4.4.0
4
+
5
+ * Redis cluster: fix cross-slot validation in pipelines. Fix ##1019.
6
+ * Add support for `XAUTOCLAIM`. See #1018.
7
+ * Properly issue `READONLY` when reconnecting to replicas. Fix #1017.
8
+ * Make `del` a noop if passed an empty list of keys. See #998.
9
+ * Add support for `ZINTER`. See #995.
10
+
11
+ # 4.3.1
12
+
13
+ * Fix password authentication against redis server 5 and older.
14
+
15
+ # 4.3.0
16
+
17
+ * Add the TYPE argument to scan and scan_each. See #985.
18
+ * Support AUTH command for ACL. See #967.
19
+
20
+ # 4.2.5
21
+
22
+ * Optimize the ruby connector write buffering. See #964.
23
+
24
+ # 4.2.4
25
+
26
+ * Fix bytesize calculations in the ruby connector, and work on a copy of the buffer. Fix #961, #962.
27
+
28
+ # 4.2.3
29
+
30
+ * Use io/wait instead of IO.select in the ruby connector. See #960.
31
+ * Use exception free non blocking IOs in the ruby connector. See #926.
32
+ * Prevent corruption of the client when an interrupt happen during inside a pipeline block. See #945.
33
+
34
+ # 4.2.2
35
+
36
+ * Fix `WATCH` support for `Redis::Distributed`. See #941.
37
+ * Fix handling of empty stream responses. See #905, #929.
38
+
39
+ # 4.2.1
40
+
41
+ * Fix `exists?` returning an actual boolean when called with multiple keys. See #918.
42
+ * Setting `Redis.exists_returns_integer = false` disables warning message about new behaviour. See #920.
43
+
44
+ # 4.2.0
45
+
46
+ * Convert commands to accept keyword arguments rather than option hashes. This both help catching typos, and reduce needless allocations.
47
+ * Deprecate the synchrony driver. It will be removed in 5.0 and hopefully maintained as a separate gem. See #915.
48
+ * Make `Redis#exists` variadic, will return an Integer if called with multiple keys.
49
+ * Add `Redis#exists?` to get a Boolean if any of the keys exists.
50
+ * `Redis#exists` when called with a single key will warn that future versions will return an Integer.
51
+ Set `Redis.exists_returns_integer = true` to opt-in to the new behavior.
52
+ * Support `keepttl` ooption in `set`. See #913.
53
+ * Optimized initialization of Redis::Cluster. See #912.
54
+ * Accept sentinel options even with string key. See #599.
55
+ * Verify TLS connections by default. See #900.
56
+ * Make `Redis#hset` variadic. It now returns an integer, not a boolean. See #910.
57
+
3
58
  # 4.1.4
4
59
 
5
60
  * Alias `Redis#disconnect` as `#close`. See #901.
@@ -9,6 +64,7 @@
9
64
  * Increase buffer size in the ruby connector. See #880.
10
65
  * Fix thread safety of `Redis.queue`. See #878.
11
66
  * Deprecate `Redis::Future#==` as it's likely to be a mistake. See #876.
67
+ * Support `KEEPTTL` option for SET command. See #913.
12
68
 
13
69
  # 4.1.3
14
70
 
data/README.md CHANGED
@@ -1,8 +1,9 @@
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.
5
5
 
6
+ See [RubyDoc.info][rubydoc] for the API docs of the latest published gem.
6
7
 
7
8
  ## Getting started
8
9
 
@@ -34,6 +35,9 @@ You can also specify connection options as a [`redis://` URL][redis-url]:
34
35
  redis = Redis.new(url: "redis://:p4ssw0rd@10.0.1.1:6380/15")
35
36
  ```
36
37
 
38
+ The client expects passwords with special chracters to be URL-encoded (i.e.
39
+ `CGI.escape(password)`).
40
+
37
41
  By default, the client will try to read the `REDIS_URL` environment variable
38
42
  and use that as URL to connect to. The above statement is therefore equivalent
39
43
  to setting this environment variable and calling `Redis.new` without arguments.
@@ -50,6 +54,12 @@ To connect to a password protected Redis instance, use:
50
54
  redis = Redis.new(password: "mysecret")
51
55
  ```
52
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
+
53
63
  The Redis class exports methods that are named identical to the commands
54
64
  they execute. The arguments these methods accept are often identical to
55
65
  the arguments specified on the [Redis website][redis-commands]. For
@@ -147,8 +157,8 @@ redis.mget('{key}1', '{key}2')
147
157
 
148
158
  ## Storing objects
149
159
 
150
- Redis only stores strings as values. If you want to store an object, you
151
- can use a serialization mechanism such as JSON:
160
+ Redis "string" types can be used to store serialized Ruby objects, for
161
+ example with JSON:
152
162
 
153
163
  ```ruby
154
164
  require "json"
@@ -261,6 +271,7 @@ All timeout values are specified in seconds.
261
271
  When using pub/sub, you can subscribe to a channel using a timeout as well:
262
272
 
263
273
  ```ruby
274
+ redis = Redis.new(reconnect_attempts: 0)
264
275
  redis.subscribe_with_timeout(5, "news") do |on|
265
276
  on.message do |channel, message|
266
277
  # ...
@@ -322,7 +333,7 @@ This library supports natively terminating client side SSL/TLS connections
322
333
  when talking to Redis via a server-side proxy such as [stunnel], [hitch],
323
334
  or [ghostunnel].
324
335
 
325
- To enable SSL support, pass the `:ssl => :true` option when configuring the
336
+ To enable SSL support, pass the `:ssl => true` option when configuring the
326
337
  Redis client, or pass in `:url => "rediss://..."` (like HTTPS for Redis).
327
338
  You will also need to pass in an `:ssl_params => { ... }` hash used to
328
339
  configure the `OpenSSL::SSL::SSLContext` object used for the connection:
@@ -435,7 +446,7 @@ redis = Redis.new(:driver => :synchrony)
435
446
  ## Testing
436
447
 
437
448
  This library is tested against recent Ruby and Redis versions.
438
- Check [Travis][travis-link] for the exact versions supported.
449
+ Check [Github Actions][gh-actions-link] for the exact versions supported.
439
450
 
440
451
  ## See Also
441
452
 
@@ -451,15 +462,14 @@ client and evangelized Redis in Rubyland. Thank you, Ezra.
451
462
  ## Contributing
452
463
 
453
464
  [Fork the project](https://github.com/redis/redis-rb) and send pull
454
- requests. You can also ask for help at `#redis-rb` on Freenode.
455
-
456
-
457
- [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
458
- [inchpages-link]: https://inch-ci.org/github/redis/redis-rb
459
- [redis-commands]: https://redis.io/commands
460
- [redis-home]: https://redis.io
461
- [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
462
- [travis-home]: https://travis-ci.org/
463
- [travis-image]: https://secure.travis-ci.org/redis/redis-rb.svg?branch=master
464
- [travis-link]: https://travis-ci.org/redis/redis-rb
465
- [rubydoc]: http://www.rubydoc.info/gems/redis
465
+ requests.
466
+
467
+
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.rb CHANGED
@@ -4,12 +4,26 @@ require "monitor"
4
4
  require_relative "redis/errors"
5
5
 
6
6
  class Redis
7
- def self.current
8
- @current ||= Redis.new
7
+ class << self
8
+ attr_reader :exists_returns_integer
9
+
10
+ def exists_returns_integer=(value)
11
+ unless value
12
+ message = "`Redis#exists(key)` will return an Integer by default in redis-rb 4.3. The option to explicitly " \
13
+ "disable this behaviour via `Redis.exists_returns_integer` will be removed in 5.0. You should use " \
14
+ "`exists?` instead."
15
+
16
+ ::Kernel.warn(message)
17
+ end
18
+
19
+ @exists_returns_integer = value
20
+ end
21
+
22
+ attr_writer :current
9
23
  end
10
24
 
11
- def self.current=(redis)
12
- @current = redis
25
+ def self.current
26
+ @current ||= Redis.new
13
27
  end
14
28
 
15
29
  include MonitorMixin
@@ -17,17 +31,22 @@ class Redis
17
31
  # Create a new client instance
18
32
  #
19
33
  # @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.
34
+ # @option options [String] :url (value of the environment variable REDIS_URL) a Redis URL, for a TCP connection:
35
+ # `redis://:[password]@[hostname]:[port]/[db]` (password, port and database are optional), for a unix socket
36
+ # connection: `unix://[path to Redis socket]`. This overrides all other options.
21
37
  # @option options [String] :host ("127.0.0.1") server hostname
22
38
  # @option options [Integer] :port (6379) server port
23
39
  # @option options [String] :path path to server socket (overrides host and port)
24
40
  # @option options [Float] :timeout (5.0) timeout in seconds
25
41
  # @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds
42
+ # @option options [String] :username Username to authenticate against server
26
43
  # @option options [String] :password Password to authenticate against server
27
44
  # @option options [Integer] :db (0) Database to select after initial connect
28
45
  # @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
46
+ # @option options [String] :id ID for the client connection, assigns name to current connection by sending
47
+ # `CLIENT SETNAME`
48
+ # @option options [Hash, Integer] :tcp_keepalive Keepalive values, if Integer `intvl` and `probe` are calculated
49
+ # based on the value, if Hash `time`, `intvl` and `probes` can be specified as a Integer
31
50
  # @option options [Integer] :reconnect_attempts Number of attempts trying to connect
32
51
  # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
33
52
  # @option options [Array] :sentinels List of sentinels to contact
@@ -52,7 +71,7 @@ class Redis
52
71
  end
53
72
 
54
73
  # Run code with the client reconnecting
55
- def with_reconnect(val=true, &blk)
74
+ def with_reconnect(val = true, &blk)
56
75
  synchronize do |client|
57
76
  client.with_reconnect(val, &blk)
58
77
  end
@@ -125,12 +144,13 @@ class Redis
125
144
 
126
145
  # Authenticate to the server.
127
146
  #
128
- # @param [String] password must match the password specified in the
129
- # `requirepass` directive in the configuration file
147
+ # @param [Array<String>] args includes both username and password
148
+ # or only password
130
149
  # @return [String] `OK`
131
- def auth(password)
150
+ # @see https://redis.io/commands/auth AUTH command
151
+ def auth(*args)
132
152
  synchronize do |client|
133
- client.call([:auth, password])
153
+ client.call([:auth, *args])
134
154
  end
135
155
  end
136
156
 
@@ -205,7 +225,7 @@ class Redis
205
225
  def config(action, *args)
206
226
  synchronize do |client|
207
227
  client.call([:config, action] + args) do |reply|
208
- if reply.kind_of?(Array) && action == :get
228
+ if reply.is_a?(Array) && action == :get
209
229
  Hashify.call(reply)
210
230
  else
211
231
  reply
@@ -256,7 +276,7 @@ class Redis
256
276
  def flushall(options = nil)
257
277
  synchronize do |client|
258
278
  if options && options[:async]
259
- client.call([:flushall, :async])
279
+ client.call(%i[flushall async])
260
280
  else
261
281
  client.call([:flushall])
262
282
  end
@@ -271,7 +291,7 @@ class Redis
271
291
  def flushdb(options = nil)
272
292
  synchronize do |client|
273
293
  if options && options[:async]
274
- client.call([:flushdb, :async])
294
+ client.call(%i[flushdb async])
275
295
  else
276
296
  client.call([:flushdb])
277
297
  end
@@ -285,7 +305,7 @@ class Redis
285
305
  def info(cmd = nil)
286
306
  synchronize do |client|
287
307
  client.call([:info, cmd].compact) do |reply|
288
- if reply.kind_of?(String)
308
+ if reply.is_a?(String)
289
309
  reply = HashifyInfo.call(reply)
290
310
 
291
311
  if cmd && cmd.to_s == "commandstats"
@@ -358,7 +378,7 @@ class Redis
358
378
  # @param [String] subcommand e.g. `get`, `len`, `reset`
359
379
  # @param [Integer] length maximum number of entries to return
360
380
  # @return [Array<String>, Integer, String] depends on subcommand
361
- def slowlog(subcommand, length=nil)
381
+ def slowlog(subcommand, length = nil)
362
382
  synchronize do |client|
363
383
  args = [:slowlog, subcommand]
364
384
  args << length if length
@@ -383,7 +403,7 @@ class Redis
383
403
  def time
384
404
  synchronize do |client|
385
405
  client.call([:time]) do |reply|
386
- reply.map(&:to_i) if reply
406
+ reply&.map(&:to_i)
387
407
  end
388
408
  end
389
409
  end
@@ -496,9 +516,9 @@ class Redis
496
516
  # - `:replace => Boolean`: if false, raises an error if key already exists
497
517
  # @raise [Redis::CommandError]
498
518
  # @return [String] `"OK"`
499
- def restore(key, ttl, serialized_value, options = {})
519
+ def restore(key, ttl, serialized_value, replace: nil)
500
520
  args = [:restore, key, ttl, serialized_value]
501
- args << 'REPLACE' if options[:replace]
521
+ args << 'REPLACE' if replace
502
522
 
503
523
  synchronize do |client|
504
524
  client.call(args)
@@ -535,6 +555,9 @@ class Redis
535
555
  # @param [String, Array<String>] keys
536
556
  # @return [Integer] number of keys that were deleted
537
557
  def del(*keys)
558
+ keys.flatten!(1)
559
+ return 0 if keys.empty?
560
+
538
561
  synchronize do |client|
539
562
  client.call([:del] + keys)
540
563
  end
@@ -550,13 +573,43 @@ class Redis
550
573
  end
551
574
  end
552
575
 
553
- # Determine if a key exists.
576
+ # Determine how many of the keys exists.
554
577
  #
555
- # @param [String] key
578
+ # @param [String, Array<String>] keys
579
+ # @return [Integer]
580
+ def exists(*keys)
581
+ if !Redis.exists_returns_integer && keys.size == 1
582
+ if Redis.exists_returns_integer.nil?
583
+ message = "`Redis#exists(key)` will return an Integer in redis-rb 4.3. `exists?` returns a boolean, you " \
584
+ "should use it instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = " \
585
+ "true. To disable this message and keep the current (boolean) behaviour of 'exists' you can set " \
586
+ "`Redis.exists_returns_integer = false`, but this option will be removed in 5.0. " \
587
+ "(#{::Kernel.caller(1, 1).first})\n"
588
+
589
+ ::Kernel.warn(message)
590
+ end
591
+
592
+ exists?(*keys)
593
+ else
594
+ _exists(*keys)
595
+ end
596
+ end
597
+
598
+ def _exists(*keys)
599
+ synchronize do |client|
600
+ client.call([:exists, *keys])
601
+ end
602
+ end
603
+
604
+ # Determine if any of the keys exists.
605
+ #
606
+ # @param [String, Array<String>] keys
556
607
  # @return [Boolean]
557
- def exists(key)
608
+ def exists?(*keys)
558
609
  synchronize do |client|
559
- client.call([:exists, key], &Boolify)
610
+ client.call([:exists, *keys]) do |value|
611
+ value > 0
612
+ end
560
613
  end
561
614
  end
562
615
 
@@ -567,7 +620,7 @@ class Redis
567
620
  def keys(pattern = "*")
568
621
  synchronize do |client|
569
622
  client.call([:keys, pattern]) do |reply|
570
- if reply.kind_of?(String)
623
+ if reply.is_a?(String)
571
624
  reply.split(" ")
572
625
  else
573
626
  reply
@@ -663,30 +716,27 @@ class Redis
663
716
  # elements where every element is an array with the result for every
664
717
  # element specified in `:get`
665
718
  # - when `:store` is specified, the number of elements in the stored result
666
- def sort(key, options = {})
667
- args = []
719
+ def sort(key, by: nil, limit: nil, get: nil, order: nil, store: nil)
720
+ args = [:sort, key]
721
+ args << "BY" << by if by
668
722
 
669
- by = options[:by]
670
- args.concat(["BY", by]) if by
671
-
672
- limit = options[:limit]
673
- args.concat(["LIMIT"] + limit) if limit
723
+ if limit
724
+ args << "LIMIT"
725
+ args.concat(limit)
726
+ end
674
727
 
675
- get = Array(options[:get])
676
- args.concat(["GET"].product(get).flatten) unless get.empty?
728
+ get = Array(get)
729
+ get.each do |item|
730
+ args << "GET" << item
731
+ end
677
732
 
678
- order = options[:order]
679
733
  args.concat(order.split(" ")) if order
680
-
681
- store = options[:store]
682
- args.concat(["STORE", store]) if store
734
+ args << "STORE" << store if store
683
735
 
684
736
  synchronize do |client|
685
- client.call([:sort, key] + args) do |reply|
737
+ client.call(args) do |reply|
686
738
  if get.size > 1 && !store
687
- if reply
688
- reply.each_slice(get.size).to_a
689
- end
739
+ reply.each_slice(get.size).to_a if reply
690
740
  else
691
741
  reply
692
742
  end
@@ -786,27 +836,21 @@ class Redis
786
836
  # - `:px => Integer`: Set the specified expire time, in milliseconds.
787
837
  # - `:nx => true`: Only set the key if it does not already exist.
788
838
  # - `:xx => true`: Only set the key if it already exist.
839
+ # - `:keepttl => true`: Retain the time to live associated with the key.
789
840
  # @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
841
+ def set(key, value, ex: nil, px: nil, nx: nil, xx: nil, keepttl: nil)
842
+ args = [:set, key, value.to_s]
843
+ args << "EX" << ex if ex
844
+ args << "PX" << px if px
845
+ args << "NX" if nx
846
+ args << "XX" if xx
847
+ args << "KEEPTTL" if keepttl
804
848
 
805
849
  synchronize do |client|
806
850
  if nx || xx
807
- client.call([:set, key, value.to_s] + args, &BoolifySet)
851
+ client.call(args, &BoolifySet)
808
852
  else
809
- client.call([:set, key, value.to_s] + args)
853
+ client.call(args)
810
854
  end
811
855
  end
812
856
  end
@@ -888,7 +932,7 @@ class Redis
888
932
  # @see #mapped_msetnx
889
933
  def msetnx(*args)
890
934
  synchronize do |client|
891
- client.call([:msetnx] + args, &Boolify)
935
+ client.call([:msetnx, *args], &Boolify)
892
936
  end
893
937
  end
894
938
 
@@ -928,7 +972,7 @@ class Redis
928
972
  # @see #mapped_mget
929
973
  def mget(*keys, &blk)
930
974
  synchronize do |client|
931
- client.call([:mget] + keys, &blk)
975
+ client.call([:mget, *keys], &blk)
932
976
  end
933
977
  end
934
978
 
@@ -944,7 +988,7 @@ class Redis
944
988
  # @see #mget
945
989
  def mapped_mget(*keys)
946
990
  mget(*keys) do |reply|
947
- if reply.kind_of?(Array)
991
+ if reply.is_a?(Array)
948
992
  Hash[keys.zip(reply)]
949
993
  else
950
994
  reply
@@ -1031,7 +1075,7 @@ class Redis
1031
1075
  # @return [Integer] the length of the string stored in `destkey`
1032
1076
  def bitop(operation, destkey, *keys)
1033
1077
  synchronize do |client|
1034
- client.call([:bitop, operation, destkey] + keys)
1078
+ client.call([:bitop, operation, destkey, *keys])
1035
1079
  end
1036
1080
  end
1037
1081
 
@@ -1043,10 +1087,8 @@ class Redis
1043
1087
  # @param [Integer] stop stop index
1044
1088
  # @return [Integer] the position of the first 1/0 bit.
1045
1089
  # -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
1090
+ def bitpos(key, bit, start = nil, stop = nil)
1091
+ raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
1050
1092
 
1051
1093
  synchronize do |client|
1052
1094
  command = [:bitpos, key, bit]
@@ -1133,23 +1175,29 @@ class Redis
1133
1175
  end
1134
1176
  end
1135
1177
 
1136
- # Remove and get the first element in a list.
1178
+ # Remove and get the first elements in a list.
1137
1179
  #
1138
1180
  # @param [String] key
1139
- # @return [String]
1140
- def lpop(key)
1181
+ # @param [Integer] count number of elements to remove
1182
+ # @return [String, Array<String>] the values of the first elements
1183
+ def lpop(key, count = nil)
1141
1184
  synchronize do |client|
1142
- client.call([:lpop, key])
1185
+ command = [:lpop, key]
1186
+ command << count if count
1187
+ client.call(command)
1143
1188
  end
1144
1189
  end
1145
1190
 
1146
- # Remove and get the last element in a list.
1191
+ # Remove and get the last elements in a list.
1147
1192
  #
1148
1193
  # @param [String] key
1149
- # @return [String]
1150
- def rpop(key)
1194
+ # @param [Integer] count number of elements to remove
1195
+ # @return [String, Array<String>] the values of the last elements
1196
+ def rpop(key, count = nil)
1151
1197
  synchronize do |client|
1152
- client.call([:rpop, key])
1198
+ command = [:rpop, key]
1199
+ command << count if count
1200
+ client.call(command)
1153
1201
  end
1154
1202
  end
1155
1203
 
@@ -1240,15 +1288,7 @@ class Redis
1240
1288
  # @return [nil, String]
1241
1289
  # - `nil` when the operation timed out
1242
1290
  # - 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
-
1291
+ def brpoplpush(source, destination, deprecated_timeout = 0, timeout: deprecated_timeout)
1252
1292
  synchronize do |client|
1253
1293
  command = [:brpoplpush, source, destination, timeout]
1254
1294
  timeout += client.timeout if timeout > 0
@@ -1455,7 +1495,7 @@ class Redis
1455
1495
  # @return [Array<String>] members in the difference
1456
1496
  def sdiff(*keys)
1457
1497
  synchronize do |client|
1458
- client.call([:sdiff] + keys)
1498
+ client.call([:sdiff, *keys])
1459
1499
  end
1460
1500
  end
1461
1501
 
@@ -1466,7 +1506,7 @@ class Redis
1466
1506
  # @return [Integer] number of elements in the resulting set
1467
1507
  def sdiffstore(destination, *keys)
1468
1508
  synchronize do |client|
1469
- client.call([:sdiffstore, destination] + keys)
1509
+ client.call([:sdiffstore, destination, *keys])
1470
1510
  end
1471
1511
  end
1472
1512
 
@@ -1476,7 +1516,7 @@ class Redis
1476
1516
  # @return [Array<String>] members in the intersection
1477
1517
  def sinter(*keys)
1478
1518
  synchronize do |client|
1479
- client.call([:sinter] + keys)
1519
+ client.call([:sinter, *keys])
1480
1520
  end
1481
1521
  end
1482
1522
 
@@ -1487,7 +1527,7 @@ class Redis
1487
1527
  # @return [Integer] number of elements in the resulting set
1488
1528
  def sinterstore(destination, *keys)
1489
1529
  synchronize do |client|
1490
- client.call([:sinterstore, destination] + keys)
1530
+ client.call([:sinterstore, destination, *keys])
1491
1531
  end
1492
1532
  end
1493
1533
 
@@ -1497,7 +1537,7 @@ class Redis
1497
1537
  # @return [Array<String>] members in the union
1498
1538
  def sunion(*keys)
1499
1539
  synchronize do |client|
1500
- client.call([:sunion] + keys)
1540
+ client.call([:sunion, *keys])
1501
1541
  end
1502
1542
  end
1503
1543
 
@@ -1508,7 +1548,7 @@ class Redis
1508
1548
  # @return [Integer] number of elements in the resulting set
1509
1549
  def sunionstore(destination, *keys)
1510
1550
  synchronize do |client|
1511
- client.call([:sunionstore, destination] + keys)
1551
+ client.call([:sunionstore, destination, *keys])
1512
1552
  end
1513
1553
  end
1514
1554
 
@@ -1557,31 +1597,20 @@ class Redis
1557
1597
  # pairs that were **added** to the sorted set.
1558
1598
  # - `Float` when option :incr is specified, holding the score of the member
1559
1599
  # 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
1600
+ def zadd(key, *args, nx: nil, xx: nil, ch: nil, incr: nil)
1601
+ command = [:zadd, key]
1602
+ command << "NX" if nx
1603
+ command << "XX" if xx
1604
+ command << "CH" if ch
1605
+ command << "INCR" if incr
1577
1606
 
1578
1607
  synchronize do |client|
1579
1608
  if args.size == 1 && args[0].is_a?(Array)
1580
1609
  # Variadic: return float if INCR, integer if !INCR
1581
- client.call([:zadd, key] + zadd_options + args[0], &(incr ? Floatify : nil))
1610
+ client.call(command + args[0], &(incr ? Floatify : nil))
1582
1611
  elsif args.size == 2
1583
1612
  # Single pair: return float if INCR, boolean if !INCR
1584
- client.call([:zadd, key] + zadd_options + args, &(incr ? Floatify : Boolify))
1613
+ client.call(command + args, &(incr ? Floatify : Boolify))
1585
1614
  else
1586
1615
  raise ArgumentError, "wrong number of arguments"
1587
1616
  end
@@ -1752,10 +1781,8 @@ class Redis
1752
1781
  # @return [Array<String>, Array<[String, Float]>]
1753
1782
  # - when `:with_scores` is not specified, an array of members
1754
1783
  # - 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]
1784
+ def zrange(key, start, stop, withscores: false, with_scores: withscores)
1785
+ args = [:zrange, key, start, stop]
1759
1786
 
1760
1787
  if with_scores
1761
1788
  args << "WITHSCORES"
@@ -1763,7 +1790,7 @@ class Redis
1763
1790
  end
1764
1791
 
1765
1792
  synchronize do |client|
1766
- client.call([:zrange, key, start, stop] + args, &block)
1793
+ client.call(args, &block)
1767
1794
  end
1768
1795
  end
1769
1796
 
@@ -1778,10 +1805,8 @@ class Redis
1778
1805
  # # => [["b", 64.0], ["a", 32.0]]
1779
1806
  #
1780
1807
  # @see #zrange
1781
- def zrevrange(key, start, stop, options = {})
1782
- args = []
1783
-
1784
- with_scores = options[:with_scores] || options[:withscores]
1808
+ def zrevrange(key, start, stop, withscores: false, with_scores: withscores)
1809
+ args = [:zrevrange, key, start, stop]
1785
1810
 
1786
1811
  if with_scores
1787
1812
  args << "WITHSCORES"
@@ -1789,7 +1814,7 @@ class Redis
1789
1814
  end
1790
1815
 
1791
1816
  synchronize do |client|
1792
- client.call([:zrevrange, key, start, stop] + args, &block)
1817
+ client.call(args, &block)
1793
1818
  end
1794
1819
  end
1795
1820
 
@@ -1880,14 +1905,16 @@ class Redis
1880
1905
  # `count` members
1881
1906
  #
1882
1907
  # @return [Array<String>, Array<[String, Float]>]
1883
- def zrangebylex(key, min, max, options = {})
1884
- args = []
1908
+ def zrangebylex(key, min, max, limit: nil)
1909
+ args = [:zrangebylex, key, min, max]
1885
1910
 
1886
- limit = options[:limit]
1887
- args.concat(["LIMIT"] + limit) if limit
1911
+ if limit
1912
+ args << "LIMIT"
1913
+ args.concat(limit)
1914
+ end
1888
1915
 
1889
1916
  synchronize do |client|
1890
- client.call([:zrangebylex, key, min, max] + args)
1917
+ client.call(args)
1891
1918
  end
1892
1919
  end
1893
1920
 
@@ -1902,14 +1929,16 @@ class Redis
1902
1929
  # # => ["abbygail", "abby"]
1903
1930
  #
1904
1931
  # @see #zrangebylex
1905
- def zrevrangebylex(key, max, min, options = {})
1906
- args = []
1932
+ def zrevrangebylex(key, max, min, limit: nil)
1933
+ args = [:zrevrangebylex, key, max, min]
1907
1934
 
1908
- limit = options[:limit]
1909
- args.concat(["LIMIT"] + limit) if limit
1935
+ if limit
1936
+ args << "LIMIT"
1937
+ args.concat(limit)
1938
+ end
1910
1939
 
1911
1940
  synchronize do |client|
1912
- client.call([:zrevrangebylex, key, max, min] + args)
1941
+ client.call(args)
1913
1942
  end
1914
1943
  end
1915
1944
 
@@ -1940,21 +1969,21 @@ class Redis
1940
1969
  # @return [Array<String>, Array<[String, Float]>]
1941
1970
  # - when `:with_scores` is not specified, an array of members
1942
1971
  # - 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]
1972
+ def zrangebyscore(key, min, max, withscores: false, with_scores: withscores, limit: nil)
1973
+ args = [:zrangebyscore, key, min, max]
1947
1974
 
1948
1975
  if with_scores
1949
1976
  args << "WITHSCORES"
1950
1977
  block = FloatifyPairs
1951
1978
  end
1952
1979
 
1953
- limit = options[:limit]
1954
- args.concat(["LIMIT"] + limit) if limit
1980
+ if limit
1981
+ args << "LIMIT"
1982
+ args.concat(limit)
1983
+ end
1955
1984
 
1956
1985
  synchronize do |client|
1957
- client.call([:zrangebyscore, key, min, max] + args, &block)
1986
+ client.call(args, &block)
1958
1987
  end
1959
1988
  end
1960
1989
 
@@ -1972,21 +2001,21 @@ class Redis
1972
2001
  # # => [["b", 64.0], ["a", 32.0]]
1973
2002
  #
1974
2003
  # @see #zrangebyscore
1975
- def zrevrangebyscore(key, max, min, options = {})
1976
- args = []
1977
-
1978
- with_scores = options[:with_scores] || options[:withscores]
2004
+ def zrevrangebyscore(key, max, min, withscores: false, with_scores: withscores, limit: nil)
2005
+ args = [:zrevrangebyscore, key, max, min]
1979
2006
 
1980
2007
  if with_scores
1981
- args << ["WITHSCORES"]
2008
+ args << "WITHSCORES"
1982
2009
  block = FloatifyPairs
1983
2010
  end
1984
2011
 
1985
- limit = options[:limit]
1986
- args.concat(["LIMIT"] + limit) if limit
2012
+ if limit
2013
+ args << "LIMIT"
2014
+ args.concat(limit)
2015
+ end
1987
2016
 
1988
2017
  synchronize do |client|
1989
- client.call([:zrevrangebyscore, key, max, min] + args, &block)
2018
+ client.call(args, &block)
1990
2019
  end
1991
2020
  end
1992
2021
 
@@ -2036,6 +2065,45 @@ class Redis
2036
2065
  end
2037
2066
  end
2038
2067
 
2068
+ # Return the intersection of multiple sorted sets
2069
+ #
2070
+ # @example Retrieve the intersection of `2*zsetA` and `1*zsetB`
2071
+ # redis.zinter("zsetA", "zsetB", :weights => [2.0, 1.0])
2072
+ # # => ["v1", "v2"]
2073
+ # @example Retrieve the intersection of `2*zsetA` and `1*zsetB`, and their scores
2074
+ # redis.zinter("zsetA", "zsetB", :weights => [2.0, 1.0], :with_scores => true)
2075
+ # # => [["v1", 3.0], ["v2", 6.0]]
2076
+ #
2077
+ # @param [String, Array<String>] keys one or more keys to intersect
2078
+ # @param [Hash] options
2079
+ # - `:weights => [Float, Float, ...]`: weights to associate with source
2080
+ # sorted sets
2081
+ # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
2082
+ # - `:with_scores => true`: include scores in output
2083
+ #
2084
+ # @return [Array<String>, Array<[String, Float]>]
2085
+ # - when `:with_scores` is not specified, an array of members
2086
+ # - when `:with_scores` is specified, an array with `[member, score]` pairs
2087
+ def zinter(*keys, weights: nil, aggregate: nil, with_scores: false)
2088
+ args = [:zinter, keys.size, *keys]
2089
+
2090
+ if weights
2091
+ args << "WEIGHTS"
2092
+ args.concat(weights)
2093
+ end
2094
+
2095
+ args << "AGGREGATE" << aggregate if aggregate
2096
+
2097
+ if with_scores
2098
+ args << "WITHSCORES"
2099
+ block = FloatifyPairs
2100
+ end
2101
+
2102
+ synchronize do |client|
2103
+ client.call(args, &block)
2104
+ end
2105
+ end
2106
+
2039
2107
  # Intersect multiple sorted sets and store the resulting sorted set in a new
2040
2108
  # key.
2041
2109
  #
@@ -2050,17 +2118,18 @@ class Redis
2050
2118
  # sorted sets
2051
2119
  # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
2052
2120
  # @return [Integer] number of elements in the resulting sorted set
2053
- def zinterstore(destination, keys, options = {})
2054
- args = []
2121
+ def zinterstore(destination, keys, weights: nil, aggregate: nil)
2122
+ args = [:zinterstore, destination, keys.size, *keys]
2055
2123
 
2056
- weights = options[:weights]
2057
- args.concat(["WEIGHTS"] + weights) if weights
2124
+ if weights
2125
+ args << "WEIGHTS"
2126
+ args.concat(weights)
2127
+ end
2058
2128
 
2059
- aggregate = options[:aggregate]
2060
- args.concat(["AGGREGATE", aggregate]) if aggregate
2129
+ args << "AGGREGATE" << aggregate if aggregate
2061
2130
 
2062
2131
  synchronize do |client|
2063
- client.call([:zinterstore, destination, keys.size] + keys + args)
2132
+ client.call(args)
2064
2133
  end
2065
2134
  end
2066
2135
 
@@ -2077,17 +2146,18 @@ class Redis
2077
2146
  # sorted sets
2078
2147
  # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
2079
2148
  # @return [Integer] number of elements in the resulting sorted set
2080
- def zunionstore(destination, keys, options = {})
2081
- args = []
2149
+ def zunionstore(destination, keys, weights: nil, aggregate: nil)
2150
+ args = [:zunionstore, destination, keys.size, *keys]
2082
2151
 
2083
- weights = options[:weights]
2084
- args.concat(["WEIGHTS"] + weights) if weights
2152
+ if weights
2153
+ args << "WEIGHTS"
2154
+ args.concat(weights)
2155
+ end
2085
2156
 
2086
- aggregate = options[:aggregate]
2087
- args.concat(["AGGREGATE", aggregate]) if aggregate
2157
+ args << "AGGREGATE" << aggregate if aggregate
2088
2158
 
2089
2159
  synchronize do |client|
2090
- client.call([:zunionstore, destination, keys.size] + keys + args)
2160
+ client.call(args)
2091
2161
  end
2092
2162
  end
2093
2163
 
@@ -2101,15 +2171,20 @@ class Redis
2101
2171
  end
2102
2172
  end
2103
2173
 
2104
- # Set the string value of a hash field.
2174
+ # Set one or more hash values.
2175
+ #
2176
+ # @example
2177
+ # redis.hset("hash", "f1", "v1", "f2", "v2") # => 2
2178
+ # redis.hset("hash", { "f1" => "v1", "f2" => "v2" }) # => 2
2105
2179
  #
2106
2180
  # @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)
2181
+ # @param [Array<String> | Hash<String, String>] attrs array or hash of fields and values
2182
+ # @return [Integer] The number of fields that were added to the hash
2183
+ def hset(key, *attrs)
2184
+ attrs = attrs.first.flatten if attrs.size == 1 && attrs.first.is_a?(Hash)
2185
+
2111
2186
  synchronize do |client|
2112
- client.call([:hset, key, field, value], &Boolify)
2187
+ client.call([:hset, key, *attrs])
2113
2188
  end
2114
2189
  end
2115
2190
 
@@ -2198,7 +2273,7 @@ class Redis
2198
2273
  # @see #hmget
2199
2274
  def mapped_hmget(key, *fields)
2200
2275
  hmget(key, *fields) do |reply|
2201
- if reply.kind_of?(Array)
2276
+ if reply.is_a?(Array)
2202
2277
  Hash[fields.zip(reply)]
2203
2278
  else
2204
2279
  reply
@@ -2291,20 +2366,21 @@ class Redis
2291
2366
 
2292
2367
  def subscribed?
2293
2368
  synchronize do |client|
2294
- client.kind_of? SubscribedClient
2369
+ client.is_a? SubscribedClient
2295
2370
  end
2296
2371
  end
2297
2372
 
2298
2373
  # Listen for messages published to the given channels.
2299
2374
  def subscribe(*channels, &block)
2300
- synchronize do |client|
2375
+ synchronize do |_client|
2301
2376
  _subscription(:subscribe, 0, channels, block)
2302
2377
  end
2303
2378
  end
2304
2379
 
2305
- # Listen for messages published to the given channels. Throw a timeout error if there is no messages for a timeout period.
2380
+ # Listen for messages published to the given channels. Throw a timeout error
2381
+ # if there is no messages for a timeout period.
2306
2382
  def subscribe_with_timeout(timeout, *channels, &block)
2307
- synchronize do |client|
2383
+ synchronize do |_client|
2308
2384
  _subscription(:subscribe_with_timeout, timeout, channels, block)
2309
2385
  end
2310
2386
  end
@@ -2312,21 +2388,23 @@ class Redis
2312
2388
  # Stop listening for messages posted to the given channels.
2313
2389
  def unsubscribe(*channels)
2314
2390
  synchronize do |client|
2315
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2391
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2392
+
2316
2393
  client.unsubscribe(*channels)
2317
2394
  end
2318
2395
  end
2319
2396
 
2320
2397
  # Listen for messages published to channels matching the given patterns.
2321
2398
  def psubscribe(*channels, &block)
2322
- synchronize do |client|
2399
+ synchronize do |_client|
2323
2400
  _subscription(:psubscribe, 0, channels, block)
2324
2401
  end
2325
2402
  end
2326
2403
 
2327
- # Listen for messages published to channels matching the given patterns. Throw a timeout error if there is no messages for a timeout period.
2404
+ # Listen for messages published to channels matching the given patterns.
2405
+ # Throw a timeout error if there is no messages for a timeout period.
2328
2406
  def psubscribe_with_timeout(timeout, *channels, &block)
2329
- synchronize do |client|
2407
+ synchronize do |_client|
2330
2408
  _subscription(:psubscribe_with_timeout, timeout, channels, block)
2331
2409
  end
2332
2410
  end
@@ -2334,7 +2412,8 @@ class Redis
2334
2412
  # Stop listening for messages posted to channels matching the given patterns.
2335
2413
  def punsubscribe(*channels)
2336
2414
  synchronize do |client|
2337
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2415
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2416
+
2338
2417
  client.punsubscribe(*channels)
2339
2418
  end
2340
2419
  end
@@ -2379,7 +2458,7 @@ class Redis
2379
2458
  # @see #multi
2380
2459
  def watch(*keys)
2381
2460
  synchronize do |client|
2382
- res = client.call([:watch] + keys)
2461
+ res = client.call([:watch, *keys])
2383
2462
 
2384
2463
  if block_given?
2385
2464
  begin
@@ -2409,14 +2488,13 @@ class Redis
2409
2488
  end
2410
2489
 
2411
2490
  def pipelined
2412
- synchronize do |client|
2491
+ synchronize do |prior_client|
2413
2492
  begin
2414
- pipeline = Pipeline.new(@client)
2415
- original, @client = @client, pipeline
2493
+ @client = Pipeline.new(prior_client)
2416
2494
  yield(self)
2417
- original.call_pipeline(@client)
2495
+ prior_client.call_pipeline(@client)
2418
2496
  ensure
2419
- @client = original
2497
+ @client = prior_client
2420
2498
  end
2421
2499
  end
2422
2500
  end
@@ -2452,17 +2530,16 @@ class Redis
2452
2530
  # @see #watch
2453
2531
  # @see #unwatch
2454
2532
  def multi
2455
- synchronize do |client|
2533
+ synchronize do |prior_client|
2456
2534
  if !block_given?
2457
- client.call([:multi])
2535
+ prior_client.call([:multi])
2458
2536
  else
2459
2537
  begin
2460
- pipeline = Pipeline::Multi.new(@client)
2461
- original, @client = @client, pipeline
2538
+ @client = Pipeline::Multi.new(prior_client)
2462
2539
  yield(self)
2463
- original.call_pipeline(pipeline)
2540
+ prior_client.call_pipeline(@client)
2464
2541
  ensure
2465
- @client = original
2542
+ @client = prior_client
2466
2543
  end
2467
2544
  end
2468
2545
  end
@@ -2609,18 +2686,13 @@ class Redis
2609
2686
  _eval(:evalsha, args)
2610
2687
  end
2611
2688
 
2612
- def _scan(command, cursor, args, options = {}, &block)
2689
+ def _scan(command, cursor, args, match: nil, count: nil, type: nil, &block)
2613
2690
  # SSCAN/ZSCAN/HSCAN already prepend the key to +args+.
2614
2691
 
2615
2692
  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
2693
+ args << "MATCH" << match if match
2694
+ args << "COUNT" << count if count
2695
+ args << "TYPE" << type if type
2624
2696
 
2625
2697
  synchronize do |client|
2626
2698
  client.call([command] + args, &block)
@@ -2635,15 +2707,19 @@ class Redis
2635
2707
  # @example Retrieve a batch of keys matching a pattern
2636
2708
  # redis.scan(4, :match => "key:1?")
2637
2709
  # # => ["92", ["key:13", "key:18"]]
2710
+ # @example Retrieve a batch of keys of a certain type
2711
+ # redis.scan(92, :type => "zset")
2712
+ # # => ["173", ["sortedset:14", "sortedset:78"]]
2638
2713
  #
2639
2714
  # @param [String, Integer] cursor the cursor of the iteration
2640
2715
  # @param [Hash] options
2641
2716
  # - `:match => String`: only return keys matching the pattern
2642
2717
  # - `:count => Integer`: return count keys at most per iteration
2718
+ # - `:type => String`: return keys only of the given type
2643
2719
  #
2644
2720
  # @return [String, Array<String>] the next cursor and all found keys
2645
- def scan(cursor, options={})
2646
- _scan(:scan, cursor, [], options)
2721
+ def scan(cursor, **options)
2722
+ _scan(:scan, cursor, [], **options)
2647
2723
  end
2648
2724
 
2649
2725
  # Scan the keyspace
@@ -2655,17 +2731,23 @@ class Redis
2655
2731
  # redis.scan_each(:match => "key:1?") {|key| puts key}
2656
2732
  # # => key:13
2657
2733
  # # => key:18
2734
+ # @example Execute block for each key of a type
2735
+ # redis.scan_each(:type => "hash") {|key| puts redis.type(key)}
2736
+ # # => "hash"
2737
+ # # => "hash"
2658
2738
  #
2659
2739
  # @param [Hash] options
2660
2740
  # - `:match => String`: only return keys matching the pattern
2661
2741
  # - `:count => Integer`: return count keys at most per iteration
2742
+ # - `:type => String`: return keys only of the given type
2662
2743
  #
2663
2744
  # @return [Enumerator] an enumerator for all found keys
2664
- def scan_each(options={}, &block)
2665
- return to_enum(:scan_each, options) unless block_given?
2745
+ def scan_each(**options, &block)
2746
+ return to_enum(:scan_each, **options) unless block_given?
2747
+
2666
2748
  cursor = 0
2667
2749
  loop do
2668
- cursor, keys = scan(cursor, options)
2750
+ cursor, keys = scan(cursor, **options)
2669
2751
  keys.each(&block)
2670
2752
  break if cursor == "0"
2671
2753
  end
@@ -2682,8 +2764,8 @@ class Redis
2682
2764
  # - `:count => Integer`: return count keys at most per iteration
2683
2765
  #
2684
2766
  # @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|
2767
+ def hscan(key, cursor, **options)
2768
+ _scan(:hscan, cursor, [key], **options) do |reply|
2687
2769
  [reply[0], reply[1].each_slice(2).to_a]
2688
2770
  end
2689
2771
  end
@@ -2699,11 +2781,12 @@ class Redis
2699
2781
  # - `:count => Integer`: return count keys at most per iteration
2700
2782
  #
2701
2783
  # @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?
2784
+ def hscan_each(key, **options, &block)
2785
+ return to_enum(:hscan_each, key, **options) unless block_given?
2786
+
2704
2787
  cursor = 0
2705
2788
  loop do
2706
- cursor, values = hscan(key, cursor, options)
2789
+ cursor, values = hscan(key, cursor, **options)
2707
2790
  values.each(&block)
2708
2791
  break if cursor == "0"
2709
2792
  end
@@ -2721,8 +2804,8 @@ class Redis
2721
2804
  #
2722
2805
  # @return [String, Array<[String, Float]>] the next cursor and all found
2723
2806
  # members and scores
2724
- def zscan(key, cursor, options={})
2725
- _scan(:zscan, cursor, [key], options) do |reply|
2807
+ def zscan(key, cursor, **options)
2808
+ _scan(:zscan, cursor, [key], **options) do |reply|
2726
2809
  [reply[0], FloatifyPairs.call(reply[1])]
2727
2810
  end
2728
2811
  end
@@ -2738,11 +2821,12 @@ class Redis
2738
2821
  # - `:count => Integer`: return count keys at most per iteration
2739
2822
  #
2740
2823
  # @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?
2824
+ def zscan_each(key, **options, &block)
2825
+ return to_enum(:zscan_each, key, **options) unless block_given?
2826
+
2743
2827
  cursor = 0
2744
2828
  loop do
2745
- cursor, values = zscan(key, cursor, options)
2829
+ cursor, values = zscan(key, cursor, **options)
2746
2830
  values.each(&block)
2747
2831
  break if cursor == "0"
2748
2832
  end
@@ -2759,8 +2843,8 @@ class Redis
2759
2843
  # - `:count => Integer`: return count keys at most per iteration
2760
2844
  #
2761
2845
  # @return [String, Array<String>] the next cursor and all found members
2762
- def sscan(key, cursor, options={})
2763
- _scan(:sscan, cursor, [key], options)
2846
+ def sscan(key, cursor, **options)
2847
+ _scan(:sscan, cursor, [key], **options)
2764
2848
  end
2765
2849
 
2766
2850
  # Scan a set
@@ -2774,11 +2858,12 @@ class Redis
2774
2858
  # - `:count => Integer`: return count keys at most per iteration
2775
2859
  #
2776
2860
  # @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?
2861
+ def sscan_each(key, **options, &block)
2862
+ return to_enum(:sscan_each, key, **options) unless block_given?
2863
+
2779
2864
  cursor = 0
2780
2865
  loop do
2781
- cursor, keys = sscan(key, cursor, options)
2866
+ cursor, keys = sscan(key, cursor, **options)
2782
2867
  keys.each(&block)
2783
2868
  break if cursor == "0"
2784
2869
  end
@@ -2842,12 +2927,12 @@ class Redis
2842
2927
  end
2843
2928
  end
2844
2929
 
2845
-
2846
2930
  # Query a sorted set representing a geospatial index to fetch members matching a
2847
2931
  # given maximum distance from a point
2848
2932
  #
2849
2933
  # @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
2934
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest
2935
+ # or the farthest to the nearest relative to the center
2851
2936
  # @param [Integer] count limit the results to the first N matching items
2852
2937
  # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2853
2938
  # @return [Array<String>] may be changed with `options`
@@ -2864,7 +2949,8 @@ class Redis
2864
2949
  # given maximum distance from an already existing member
2865
2950
  #
2866
2951
  # @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
2952
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest
2953
+ # to the nearest relative to the center
2868
2954
  # @param [Integer] count limit the results to the first N matching items
2869
2955
  # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2870
2956
  # @return [Array<String>] may be changed with `options`
@@ -2881,7 +2967,8 @@ class Redis
2881
2967
  #
2882
2968
  # @param [String] key
2883
2969
  # @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
2970
+ # @return [Array<Array<String>, nil>] returns array of elements, where each
2971
+ # element is either array of longitude and latitude or nil
2885
2972
  def geopos(key, member)
2886
2973
  synchronize do |client|
2887
2974
  client.call([:geopos, key, member])
@@ -2945,10 +3032,14 @@ class Redis
2945
3032
  # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen or not
2946
3033
  #
2947
3034
  # @return [String] the entry id
2948
- def xadd(key, entry, opts = {})
3035
+ def xadd(key, entry, approximate: nil, maxlen: nil, id: '*')
2949
3036
  args = [:xadd, key]
2950
- args.concat(['MAXLEN', (opts[:approximate] ? '~' : nil), opts[:maxlen]].compact) if opts[:maxlen]
2951
- args << (opts[:id] || '*')
3037
+ if maxlen
3038
+ args << "MAXLEN"
3039
+ args << "~" if approximate
3040
+ args << maxlen
3041
+ end
3042
+ args << id
2952
3043
  args.concat(entry.to_a.flatten)
2953
3044
  synchronize { |client| client.call(args) }
2954
3045
  end
@@ -3003,8 +3094,8 @@ class Redis
3003
3094
  # @param count [Integer] the number of entries as limit
3004
3095
  #
3005
3096
  # @return [Array<Array<String, Hash>>] the ids and entries pairs
3006
- def xrange(key, start = '-', _end = '+', count: nil)
3007
- args = [:xrange, key, start, _end]
3097
+ def xrange(key, start = '-', range_end = '+', count: nil)
3098
+ args = [:xrange, key, start, range_end]
3008
3099
  args.concat(['COUNT', count]) if count
3009
3100
  synchronize { |client| client.call(args, &HashifyStreamEntries) }
3010
3101
  end
@@ -3026,8 +3117,8 @@ class Redis
3026
3117
  # @params count [Integer] the number of entries as limit
3027
3118
  #
3028
3119
  # @return [Array<Array<String, Hash>>] the ids and entries pairs
3029
- def xrevrange(key, _end = '+', start = '-', count: nil)
3030
- args = [:xrevrange, key, _end, start]
3120
+ def xrevrange(key, range_end = '+', start = '-', count: nil)
3121
+ args = [:xrevrange, key, range_end, start]
3031
3122
  args.concat(['COUNT', count]) if count
3032
3123
  synchronize { |client| client.call(args, &HashifyStreamEntries) }
3033
3124
  end
@@ -3119,12 +3210,12 @@ class Redis
3119
3210
  # @option opts [Boolean] :noack whether message loss is acceptable or not
3120
3211
  #
3121
3212
  # @return [Hash{String => Hash{String => Hash}}] the entries
3122
- def xreadgroup(group, consumer, keys, ids, opts = {})
3213
+ def xreadgroup(group, consumer, keys, ids, count: nil, block: nil, noack: nil)
3123
3214
  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])
3215
+ args << 'COUNT' << count if count
3216
+ args << 'BLOCK' << block.to_i if block
3217
+ args << 'NOACK' if noack
3218
+ _xread(args, keys, ids, block)
3128
3219
  end
3129
3220
 
3130
3221
  # Removes one or multiple entries from the pending entries list of a stream consumer group.
@@ -3189,6 +3280,38 @@ class Redis
3189
3280
  synchronize { |client| client.call(args, &blk) }
3190
3281
  end
3191
3282
 
3283
+ # Transfers ownership of pending stream entries that match the specified criteria.
3284
+ #
3285
+ # @example Claim next pending message stuck > 5 minutes and mark as retry
3286
+ # redis.xautoclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0')
3287
+ # @example Claim 50 next pending messages stuck > 5 minutes and mark as retry
3288
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0', count: 50)
3289
+ # @example Claim next pending message stuck > 5 minutes and don't mark as retry
3290
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0', justid: true)
3291
+ # @example Claim next pending message after this id stuck > 5 minutes and mark as retry
3292
+ # redis.xautoclaim('mystream', 'mygroup', 'consumer1', 3600000, '1641321233-0')
3293
+ #
3294
+ # @param key [String] the stream key
3295
+ # @param group [String] the consumer group name
3296
+ # @param consumer [String] the consumer name
3297
+ # @param min_idle_time [Integer] the number of milliseconds
3298
+ # @param start [String] entry id to start scanning from or 0-0 for everything
3299
+ # @param count [Integer] number of messages to claim (default 1)
3300
+ # @param justid [Boolean] whether to fetch just an array of entry ids or not.
3301
+ # Does not increment retry count when true
3302
+ #
3303
+ # @return [Hash{String => Hash}] the entries successfully claimed
3304
+ # @return [Array<String>] the entry ids successfully claimed if justid option is `true`
3305
+ def xautoclaim(key, group, consumer, min_idle_time, start, count: nil, justid: false)
3306
+ args = [:xautoclaim, key, group, consumer, min_idle_time, start]
3307
+ if count
3308
+ args << 'COUNT' << count.to_s
3309
+ end
3310
+ args << 'JUSTID' if justid
3311
+ blk = justid ? HashifyStreamAutoclaimJustId : HashifyStreamAutoclaim
3312
+ synchronize { |client| client.call(args, &blk) }
3313
+ end
3314
+
3192
3315
  # Fetches not acknowledging pending entries
3193
3316
  #
3194
3317
  # @example With key and group
@@ -3234,8 +3357,8 @@ class Redis
3234
3357
  when "get-master-addr-by-name"
3235
3358
  reply
3236
3359
  else
3237
- if reply.kind_of?(Array)
3238
- if reply[0].kind_of?(Array)
3360
+ if reply.is_a?(Array)
3361
+ if reply[0].is_a?(Array)
3239
3362
  reply.map(&Hashify)
3240
3363
  else
3241
3364
  Hashify.call(reply)
@@ -3259,12 +3382,17 @@ class Redis
3259
3382
  def cluster(subcommand, *args)
3260
3383
  subcommand = subcommand.to_s.downcase
3261
3384
  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
3385
+ when 'slots'
3386
+ HashifyClusterSlots
3387
+ when 'nodes'
3388
+ HashifyClusterNodes
3389
+ when 'slaves'
3390
+ HashifyClusterSlaves
3391
+ when 'info'
3392
+ HashifyInfo
3393
+ else
3394
+ Noop
3395
+ end
3268
3396
 
3269
3397
  # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
3270
3398
  block = Noop unless @cluster_mode
@@ -3299,21 +3427,21 @@ class Redis
3299
3427
  return @original_client.connection_info if @cluster_mode
3300
3428
 
3301
3429
  {
3302
- host: @original_client.host,
3303
- port: @original_client.port,
3304
- db: @original_client.db,
3305
- id: @original_client.id,
3430
+ host: @original_client.host,
3431
+ port: @original_client.port,
3432
+ db: @original_client.db,
3433
+ id: @original_client.id,
3306
3434
  location: @original_client.location
3307
3435
  }
3308
3436
  end
3309
3437
 
3310
- def method_missing(command, *args)
3438
+ def method_missing(command, *args) # rubocop:disable Style/MissingRespondToMissing
3311
3439
  synchronize do |client|
3312
3440
  client.call([command] + args)
3313
3441
  end
3314
3442
  end
3315
3443
 
3316
- private
3444
+ private
3317
3445
 
3318
3446
  # Commands returning 1 for true and 0 for false may be executed in a pipeline
3319
3447
  # where the method call will return nil. Propagate the nil instead of falsely
@@ -3385,18 +3513,35 @@ private
3385
3513
  end
3386
3514
  }
3387
3515
 
3516
+ EMPTY_STREAM_RESPONSE = [nil].freeze
3517
+ private_constant :EMPTY_STREAM_RESPONSE
3518
+
3388
3519
  HashifyStreamEntries = lambda { |reply|
3389
- reply.map do |entry_id, values|
3520
+ reply.compact.map do |entry_id, values|
3390
3521
  [entry_id, values.each_slice(2).to_h]
3391
3522
  end
3392
3523
  }
3393
3524
 
3525
+ HashifyStreamAutoclaim = lambda { |reply|
3526
+ {
3527
+ 'next' => reply[0],
3528
+ 'entries' => reply[1].map { |entry| [entry[0], entry[1].each_slice(2).to_h] }
3529
+ }
3530
+ }
3531
+
3532
+ HashifyStreamAutoclaimJustId = lambda { |reply|
3533
+ {
3534
+ 'next' => reply[0],
3535
+ 'entries' => reply[1]
3536
+ }
3537
+ }
3538
+
3394
3539
  HashifyStreamPendings = lambda { |reply|
3395
3540
  {
3396
- 'size' => reply[0],
3541
+ 'size' => reply[0],
3397
3542
  'min_entry_id' => reply[1],
3398
3543
  'max_entry_id' => reply[2],
3399
- 'consumers' => reply[3].nil? ? {} : reply[3].to_h
3544
+ 'consumers' => reply[3].nil? ? {} : reply[3].to_h
3400
3545
  }
3401
3546
  }
3402
3547
 
@@ -3405,8 +3550,8 @@ private
3405
3550
  {
3406
3551
  'entry_id' => arr[0],
3407
3552
  'consumer' => arr[1],
3408
- 'elapsed' => arr[2],
3409
- 'count' => arr[3]
3553
+ 'elapsed' => arr[2],
3554
+ 'count' => arr[3]
3410
3555
  }
3411
3556
  end
3412
3557
  }
@@ -3414,15 +3559,15 @@ private
3414
3559
  HashifyClusterNodeInfo = lambda { |str|
3415
3560
  arr = str.split(' ')
3416
3561
  {
3417
- 'node_id' => arr[0],
3418
- 'ip_port' => arr[1],
3419
- 'flags' => arr[2].split(','),
3562
+ 'node_id' => arr[0],
3563
+ 'ip_port' => arr[1],
3564
+ 'flags' => arr[2].split(','),
3420
3565
  '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('-'))
3566
+ 'ping_sent' => arr[4],
3567
+ 'pong_recv' => arr[5],
3568
+ 'config_epoch' => arr[6],
3569
+ 'link_state' => arr[7],
3570
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
3426
3571
  }
3427
3572
  }
3428
3573
 
@@ -3433,9 +3578,9 @@ private
3433
3578
  replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
3434
3579
  {
3435
3580
  'start_slot' => first_slot,
3436
- 'end_slot' => last_slot,
3437
- 'master' => master,
3438
- 'replicas' => replicas
3581
+ 'end_slot' => last_slot,
3582
+ 'master' => master,
3583
+ 'replicas' => replicas
3439
3584
  }
3440
3585
  end
3441
3586
  }