redis 4.0.0.rc1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +143 -3
  3. data/README.md +127 -18
  4. data/lib/redis/client.rb +150 -93
  5. data/lib/redis/cluster/command.rb +81 -0
  6. data/lib/redis/cluster/command_loader.rb +34 -0
  7. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  8. data/lib/redis/cluster/node.rb +108 -0
  9. data/lib/redis/cluster/node_key.rb +31 -0
  10. data/lib/redis/cluster/node_loader.rb +37 -0
  11. data/lib/redis/cluster/option.rb +93 -0
  12. data/lib/redis/cluster/slot.rb +86 -0
  13. data/lib/redis/cluster/slot_loader.rb +49 -0
  14. data/lib/redis/cluster.rb +291 -0
  15. data/lib/redis/connection/command_helper.rb +3 -2
  16. data/lib/redis/connection/hiredis.rb +4 -3
  17. data/lib/redis/connection/registry.rb +2 -1
  18. data/lib/redis/connection/ruby.rb +123 -105
  19. data/lib/redis/connection/synchrony.rb +18 -5
  20. data/lib/redis/connection.rb +2 -0
  21. data/lib/redis/distributed.rb +955 -0
  22. data/lib/redis/errors.rb +48 -0
  23. data/lib/redis/hash_ring.rb +89 -0
  24. data/lib/redis/pipeline.rb +55 -9
  25. data/lib/redis/subscribe.rb +11 -12
  26. data/lib/redis/version.rb +3 -1
  27. data/lib/redis.rb +1242 -381
  28. metadata +34 -141
  29. data/.gitignore +0 -16
  30. data/.travis/Gemfile +0 -11
  31. data/.travis.yml +0 -71
  32. data/.yardopts +0 -3
  33. data/Gemfile +0 -3
  34. data/benchmarking/logging.rb +0 -71
  35. data/benchmarking/pipeline.rb +0 -51
  36. data/benchmarking/speed.rb +0 -21
  37. data/benchmarking/suite.rb +0 -24
  38. data/benchmarking/worker.rb +0 -71
  39. data/examples/basic.rb +0 -15
  40. data/examples/consistency.rb +0 -114
  41. data/examples/incr-decr.rb +0 -17
  42. data/examples/list.rb +0 -26
  43. data/examples/pubsub.rb +0 -37
  44. data/examples/sentinel/sentinel.conf +0 -9
  45. data/examples/sentinel/start +0 -49
  46. data/examples/sentinel.rb +0 -41
  47. data/examples/sets.rb +0 -36
  48. data/examples/unicorn/config.ru +0 -3
  49. data/examples/unicorn/unicorn.rb +0 -20
  50. data/makefile +0 -42
  51. data/redis.gemspec +0 -40
  52. data/test/bitpos_test.rb +0 -63
  53. data/test/blocking_commands_test.rb +0 -183
  54. data/test/client_test.rb +0 -59
  55. data/test/command_map_test.rb +0 -28
  56. data/test/commands_on_hashes_test.rb +0 -174
  57. data/test/commands_on_hyper_log_log_test.rb +0 -70
  58. data/test/commands_on_lists_test.rb +0 -154
  59. data/test/commands_on_sets_test.rb +0 -208
  60. data/test/commands_on_sorted_sets_test.rb +0 -444
  61. data/test/commands_on_strings_test.rb +0 -338
  62. data/test/commands_on_value_types_test.rb +0 -246
  63. data/test/connection_handling_test.rb +0 -275
  64. data/test/db/.gitkeep +0 -0
  65. data/test/encoding_test.rb +0 -14
  66. data/test/error_replies_test.rb +0 -57
  67. data/test/fork_safety_test.rb +0 -60
  68. data/test/helper.rb +0 -179
  69. data/test/helper_test.rb +0 -22
  70. data/test/internals_test.rb +0 -435
  71. data/test/persistence_control_commands_test.rb +0 -24
  72. data/test/pipelining_commands_test.rb +0 -238
  73. data/test/publish_subscribe_test.rb +0 -280
  74. data/test/remote_server_control_commands_test.rb +0 -175
  75. data/test/scanning_test.rb +0 -407
  76. data/test/scripting_test.rb +0 -76
  77. data/test/sentinel_command_test.rb +0 -78
  78. data/test/sentinel_test.rb +0 -253
  79. data/test/sorting_test.rb +0 -57
  80. data/test/ssl_test.rb +0 -69
  81. data/test/support/connection/hiredis.rb +0 -1
  82. data/test/support/connection/ruby.rb +0 -1
  83. data/test/support/connection/synchrony.rb +0 -17
  84. data/test/support/redis_mock.rb +0 -130
  85. data/test/support/ssl/gen_certs.sh +0 -31
  86. data/test/support/ssl/trusted-ca.crt +0 -25
  87. data/test/support/ssl/trusted-ca.key +0 -27
  88. data/test/support/ssl/trusted-cert.crt +0 -81
  89. data/test/support/ssl/trusted-cert.key +0 -28
  90. data/test/support/ssl/untrusted-ca.crt +0 -26
  91. data/test/support/ssl/untrusted-ca.key +0 -27
  92. data/test/support/ssl/untrusted-cert.crt +0 -82
  93. data/test/support/ssl/untrusted-cert.key +0 -28
  94. data/test/support/wire/synchrony.rb +0 -24
  95. data/test/support/wire/thread.rb +0 -5
  96. data/test/synchrony_driver.rb +0 -85
  97. data/test/test.conf.erb +0 -9
  98. data/test/thread_safety_test.rb +0 -60
  99. data/test/transactions_test.rb +0 -262
  100. data/test/unknown_commands_test.rb +0 -12
  101. data/test/url_param_test.rb +0 -136
data/lib/redis.rb CHANGED
@@ -1,14 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "monitor"
2
4
  require_relative "redis/errors"
3
5
 
4
6
  class Redis
7
+ class << self
8
+ attr_reader :exists_returns_integer
5
9
 
6
- def self.current
7
- @current ||= Redis.new
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
8
23
  end
9
24
 
10
- def self.current=(redis)
11
- @current = redis
25
+ def self.current
26
+ @current ||= Redis.new
12
27
  end
13
28
 
14
29
  include MonitorMixin
@@ -16,26 +31,36 @@ class Redis
16
31
  # Create a new client instance
17
32
  #
18
33
  # @param [Hash] options
19
- # @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.
20
37
  # @option options [String] :host ("127.0.0.1") server hostname
21
- # @option options [Fixnum] :port (6379) server port
38
+ # @option options [Integer] :port (6379) server port
22
39
  # @option options [String] :path path to server socket (overrides host and port)
23
40
  # @option options [Float] :timeout (5.0) timeout in seconds
24
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
25
43
  # @option options [String] :password Password to authenticate against server
26
- # @option options [Fixnum] :db (0) Database to select after initial connect
44
+ # @option options [Integer] :db (0) Database to select after initial connect
27
45
  # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`, `:synchrony`
28
- # @option options [String] :id ID for the client connection, assigns name to current connection by sending `CLIENT SETNAME`
29
- # @option options [Hash, Fixnum] :tcp_keepalive Keepalive values, if Fixnum `intvl` and `probe` are calculated based on the value, if Hash `time`, `intvl` and `probes` can be specified as a Fixnum
30
- # @option options [Fixnum] :reconnect_attempts Number of attempts trying to connect
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
50
+ # @option options [Integer] :reconnect_attempts Number of attempts trying to connect
31
51
  # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
32
52
  # @option options [Array] :sentinels List of sentinels to contact
33
53
  # @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave`
54
+ # @option options [Array<String, Hash{Symbol => String, Integer}>] :cluster List of cluster nodes to contact
55
+ # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not
56
+ # @option options [Class] :connector Class of custom connector
34
57
  #
35
58
  # @return [Redis] a new client instance
36
59
  def initialize(options = {})
37
60
  @options = options.dup
38
- @original_client = @client = Client.new(options)
61
+ @cluster_mode = options.key?(:cluster)
62
+ client = @cluster_mode ? Cluster : Client
63
+ @original_client = @client = client.new(options)
39
64
  @queue = Hash.new { |h, k| h[k] = [] }
40
65
 
41
66
  super() # Monitor#initialize
@@ -46,7 +71,7 @@ class Redis
46
71
  end
47
72
 
48
73
  # Run code with the client reconnecting
49
- def with_reconnect(val=true, &blk)
74
+ def with_reconnect(val = true, &blk)
50
75
  synchronize do |client|
51
76
  client.with_reconnect(val, &blk)
52
77
  end
@@ -89,7 +114,9 @@ class Redis
89
114
  # See http://redis.io/topics/pipelining for more details.
90
115
  #
91
116
  def queue(*command)
92
- @queue[Thread.current.object_id] << command
117
+ synchronize do
118
+ @queue[Thread.current.object_id] << command
119
+ end
93
120
  end
94
121
 
95
122
  # Sends all commands in the queue.
@@ -99,7 +126,12 @@ class Redis
99
126
  def commit
100
127
  synchronize do |client|
101
128
  begin
102
- client.call_pipelined(@queue[Thread.current.object_id])
129
+ pipeline = Pipeline.new(client)
130
+ @queue[Thread.current.object_id].each do |command|
131
+ pipeline.call(command)
132
+ end
133
+
134
+ client.call_pipelined(pipeline)
103
135
  ensure
104
136
  @queue.delete(Thread.current.object_id)
105
137
  end
@@ -112,18 +144,19 @@ class Redis
112
144
 
113
145
  # Authenticate to the server.
114
146
  #
115
- # @param [String] password must match the password specified in the
116
- # `requirepass` directive in the configuration file
147
+ # @param [Array<String>] args includes both username and password
148
+ # or only password
117
149
  # @return [String] `OK`
118
- def auth(password)
150
+ # @see https://redis.io/commands/auth AUTH command
151
+ def auth(*args)
119
152
  synchronize do |client|
120
- client.call([:auth, password])
153
+ client.call([:auth, *args])
121
154
  end
122
155
  end
123
156
 
124
157
  # Change the selected database for the current connection.
125
158
  #
126
- # @param [Fixnum] db zero-based index of the DB to use (0 to 15)
159
+ # @param [Integer] db zero-based index of the DB to use (0 to 15)
127
160
  # @return [String] `OK`
128
161
  def select(db)
129
162
  synchronize do |client|
@@ -134,10 +167,11 @@ class Redis
134
167
 
135
168
  # Ping the server.
136
169
  #
170
+ # @param [optional, String] message
137
171
  # @return [String] `PONG`
138
- def ping
172
+ def ping(message = nil)
139
173
  synchronize do |client|
140
- client.call([:ping])
174
+ client.call([:ping, message].compact)
141
175
  end
142
176
  end
143
177
 
@@ -191,7 +225,7 @@ class Redis
191
225
  def config(action, *args)
192
226
  synchronize do |client|
193
227
  client.call([:config, action] + args) do |reply|
194
- if reply.kind_of?(Array) && action == :get
228
+ if reply.is_a?(Array) && action == :get
195
229
  Hashify.call(reply)
196
230
  else
197
231
  reply
@@ -221,7 +255,7 @@ class Redis
221
255
 
222
256
  # Return the number of keys in the selected database.
223
257
  #
224
- # @return [Fixnum]
258
+ # @return [Integer]
225
259
  def dbsize
226
260
  synchronize do |client|
227
261
  client.call([:dbsize])
@@ -236,19 +270,31 @@ class Redis
236
270
 
237
271
  # Remove all keys from all databases.
238
272
  #
273
+ # @param [Hash] options
274
+ # - `:async => Boolean`: async flush (default: false)
239
275
  # @return [String] `OK`
240
- def flushall
276
+ def flushall(options = nil)
241
277
  synchronize do |client|
242
- client.call([:flushall])
278
+ if options && options[:async]
279
+ client.call(%i[flushall async])
280
+ else
281
+ client.call([:flushall])
282
+ end
243
283
  end
244
284
  end
245
285
 
246
286
  # Remove all keys from the current database.
247
287
  #
288
+ # @param [Hash] options
289
+ # - `:async => Boolean`: async flush (default: false)
248
290
  # @return [String] `OK`
249
- def flushdb
291
+ def flushdb(options = nil)
250
292
  synchronize do |client|
251
- client.call([:flushdb])
293
+ if options && options[:async]
294
+ client.call(%i[flushdb async])
295
+ else
296
+ client.call([:flushdb])
297
+ end
252
298
  end
253
299
  end
254
300
 
@@ -259,10 +305,8 @@ class Redis
259
305
  def info(cmd = nil)
260
306
  synchronize do |client|
261
307
  client.call([:info, cmd].compact) do |reply|
262
- if reply.kind_of?(String)
263
- reply = Hash[reply.split("\r\n").map do |line|
264
- line.split(":", 2) unless line =~ /^(#|$)/
265
- end.compact]
308
+ if reply.is_a?(String)
309
+ reply = HashifyInfo.call(reply)
266
310
 
267
311
  if cmd && cmd.to_s == "commandstats"
268
312
  # Extract nested hashes for INFO COMMANDSTATS
@@ -280,7 +324,7 @@ class Redis
280
324
 
281
325
  # Get the UNIX time stamp of the last successful save to disk.
282
326
  #
283
- # @return [Fixnum]
327
+ # @return [Integer]
284
328
  def lastsave
285
329
  synchronize do |client|
286
330
  client.call([:lastsave])
@@ -332,9 +376,9 @@ class Redis
332
376
  # Interact with the slowlog (get, len, reset)
333
377
  #
334
378
  # @param [String] subcommand e.g. `get`, `len`, `reset`
335
- # @param [Fixnum] length maximum number of entries to return
336
- # @return [Array<String>, Fixnum, String] depends on subcommand
337
- def slowlog(subcommand, length=nil)
379
+ # @param [Integer] length maximum number of entries to return
380
+ # @return [Array<String>, Integer, String] depends on subcommand
381
+ def slowlog(subcommand, length = nil)
338
382
  synchronize do |client|
339
383
  args = [:slowlog, subcommand]
340
384
  args << length if length
@@ -354,12 +398,12 @@ class Redis
354
398
  # @example
355
399
  # r.time # => [ 1333093196, 606806 ]
356
400
  #
357
- # @return [Array<Fixnum>] tuple of seconds since UNIX epoch and
401
+ # @return [Array<Integer>] tuple of seconds since UNIX epoch and
358
402
  # microseconds in the current second
359
403
  def time
360
404
  synchronize do |client|
361
405
  client.call([:time]) do |reply|
362
- reply.map(&:to_i) if reply
406
+ reply&.map(&:to_i)
363
407
  end
364
408
  end
365
409
  end
@@ -377,7 +421,7 @@ class Redis
377
421
  # Set a key's time to live in seconds.
378
422
  #
379
423
  # @param [String] key
380
- # @param [Fixnum] seconds time to live
424
+ # @param [Integer] seconds time to live
381
425
  # @return [Boolean] whether the timeout was set or not
382
426
  def expire(key, seconds)
383
427
  synchronize do |client|
@@ -388,7 +432,7 @@ class Redis
388
432
  # Set the expiration for a key as a UNIX timestamp.
389
433
  #
390
434
  # @param [String] key
391
- # @param [Fixnum] unix_time expiry time specified as a UNIX timestamp
435
+ # @param [Integer] unix_time expiry time specified as a UNIX timestamp
392
436
  # @return [Boolean] whether the timeout was set or not
393
437
  def expireat(key, unix_time)
394
438
  synchronize do |client|
@@ -399,7 +443,7 @@ class Redis
399
443
  # Get the time to live (in seconds) for a key.
400
444
  #
401
445
  # @param [String] key
402
- # @return [Fixnum] remaining time to live in seconds.
446
+ # @return [Integer] remaining time to live in seconds.
403
447
  #
404
448
  # In Redis 2.6 or older the command returns -1 if the key does not exist or if
405
449
  # the key exist but has no associated expire.
@@ -417,7 +461,7 @@ class Redis
417
461
  # Set a key's time to live in milliseconds.
418
462
  #
419
463
  # @param [String] key
420
- # @param [Fixnum] milliseconds time to live
464
+ # @param [Integer] milliseconds time to live
421
465
  # @return [Boolean] whether the timeout was set or not
422
466
  def pexpire(key, milliseconds)
423
467
  synchronize do |client|
@@ -428,7 +472,7 @@ class Redis
428
472
  # Set the expiration for a key as number of milliseconds from UNIX Epoch.
429
473
  #
430
474
  # @param [String] key
431
- # @param [Fixnum] ms_unix_time expiry time specified as number of milliseconds from UNIX Epoch.
475
+ # @param [Integer] ms_unix_time expiry time specified as number of milliseconds from UNIX Epoch.
432
476
  # @return [Boolean] whether the timeout was set or not
433
477
  def pexpireat(key, ms_unix_time)
434
478
  synchronize do |client|
@@ -439,7 +483,7 @@ class Redis
439
483
  # Get the time to live (in milliseconds) for a key.
440
484
  #
441
485
  # @param [String] key
442
- # @return [Fixnum] remaining time to live in milliseconds
486
+ # @return [Integer] remaining time to live in milliseconds
443
487
  # In Redis 2.6 or older the command returns -1 if the key does not exist or if
444
488
  # the key exist but has no associated expire.
445
489
  #
@@ -468,50 +512,104 @@ class Redis
468
512
  # @param [String] key
469
513
  # @param [String] ttl
470
514
  # @param [String] serialized_value
515
+ # @param [Hash] options
516
+ # - `:replace => Boolean`: if false, raises an error if key already exists
517
+ # @raise [Redis::CommandError]
471
518
  # @return [String] `"OK"`
472
- def restore(key, ttl, serialized_value)
519
+ def restore(key, ttl, serialized_value, replace: nil)
520
+ args = [:restore, key, ttl, serialized_value]
521
+ args << 'REPLACE' if replace
522
+
473
523
  synchronize do |client|
474
- client.call([:restore, key, ttl, serialized_value])
524
+ client.call(args)
475
525
  end
476
526
  end
477
527
 
478
528
  # Transfer a key from the connected instance to another instance.
479
529
  #
480
- # @param [String] key
530
+ # @param [String, Array<String>] key
481
531
  # @param [Hash] options
482
532
  # - `:host => String`: host of instance to migrate to
483
533
  # - `:port => Integer`: port of instance to migrate to
484
534
  # - `:db => Integer`: database to migrate to (default: same as source)
485
535
  # - `:timeout => Integer`: timeout (default: same as connection timeout)
536
+ # - `:copy => Boolean`: Do not remove the key from the local instance.
537
+ # - `:replace => Boolean`: Replace existing key on the remote instance.
486
538
  # @return [String] `"OK"`
487
539
  def migrate(key, options)
488
- host = options[:host] || raise(RuntimeError, ":host not specified")
489
- port = options[:port] || raise(RuntimeError, ":port not specified")
490
- db = (options[:db] || @client.db).to_i
491
- timeout = (options[:timeout] || @client.timeout).to_i
540
+ args = [:migrate]
541
+ args << (options[:host] || raise(':host not specified'))
542
+ args << (options[:port] || raise(':port not specified'))
543
+ args << (key.is_a?(String) ? key : '')
544
+ args << (options[:db] || @client.db).to_i
545
+ args << (options[:timeout] || @client.timeout).to_i
546
+ args << 'COPY' if options[:copy]
547
+ args << 'REPLACE' if options[:replace]
548
+ args += ['KEYS', *key] if key.is_a?(Array)
492
549
 
493
- synchronize do |client|
494
- client.call([:migrate, host, port, key, db, timeout])
495
- end
550
+ synchronize { |client| client.call(args) }
496
551
  end
497
552
 
498
553
  # Delete one or more keys.
499
554
  #
500
555
  # @param [String, Array<String>] keys
501
- # @return [Fixnum] number of keys that were deleted
556
+ # @return [Integer] number of keys that were deleted
502
557
  def del(*keys)
558
+ keys.flatten!(1)
559
+ return 0 if keys.empty?
560
+
503
561
  synchronize do |client|
504
562
  client.call([:del] + keys)
505
563
  end
506
564
  end
507
565
 
508
- # Determine if a key exists.
566
+ # Unlink one or more keys.
509
567
  #
510
- # @param [String] key
568
+ # @param [String, Array<String>] keys
569
+ # @return [Integer] number of keys that were unlinked
570
+ def unlink(*keys)
571
+ synchronize do |client|
572
+ client.call([:unlink] + keys)
573
+ end
574
+ end
575
+
576
+ # Determine how many of the keys exists.
577
+ #
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
511
607
  # @return [Boolean]
512
- def exists(key)
608
+ def exists?(*keys)
513
609
  synchronize do |client|
514
- client.call([:exists, key], &Boolify)
610
+ client.call([:exists, *keys]) do |value|
611
+ value > 0
612
+ end
515
613
  end
516
614
  end
517
615
 
@@ -522,7 +620,7 @@ class Redis
522
620
  def keys(pattern = "*")
523
621
  synchronize do |client|
524
622
  client.call([:keys, pattern]) do |reply|
525
- if reply.kind_of?(String)
623
+ if reply.is_a?(String)
526
624
  reply.split(" ")
527
625
  else
528
626
  reply
@@ -548,7 +646,7 @@ class Redis
548
646
  # # => "bar"
549
647
  #
550
648
  # @param [String] key
551
- # @param [Fixnum] db
649
+ # @param [Integer] db
552
650
  # @return [Boolean] whether the key was moved or not
553
651
  def move(key, db)
554
652
  synchronize do |client|
@@ -612,36 +710,33 @@ class Redis
612
710
  # - `:order => String`: combination of `ASC`, `DESC` and optionally `ALPHA`
613
711
  # - `:store => String`: key to store the result at
614
712
  #
615
- # @return [Array<String>, Array<Array<String>>, Fixnum]
713
+ # @return [Array<String>, Array<Array<String>>, Integer]
616
714
  # - when `:get` is not specified, or holds a single element, an array of elements
617
715
  # - when `:get` is specified, and holds more than one element, an array of
618
716
  # elements where every element is an array with the result for every
619
717
  # element specified in `:get`
620
718
  # - when `:store` is specified, the number of elements in the stored result
621
- def sort(key, options = {})
622
- args = []
623
-
624
- by = options[:by]
625
- args.concat(["BY", by]) if by
719
+ def sort(key, by: nil, limit: nil, get: nil, order: nil, store: nil)
720
+ args = [:sort, key]
721
+ args << "BY" << by if by
626
722
 
627
- limit = options[:limit]
628
- args.concat(["LIMIT"] + limit) if limit
723
+ if limit
724
+ args << "LIMIT"
725
+ args.concat(limit)
726
+ end
629
727
 
630
- get = Array(options[:get])
631
- args.concat(["GET"].product(get).flatten) unless get.empty?
728
+ get = Array(get)
729
+ get.each do |item|
730
+ args << "GET" << item
731
+ end
632
732
 
633
- order = options[:order]
634
733
  args.concat(order.split(" ")) if order
635
-
636
- store = options[:store]
637
- args.concat(["STORE", store]) if store
734
+ args << "STORE" << store if store
638
735
 
639
736
  synchronize do |client|
640
- client.call([:sort, key] + args) do |reply|
737
+ client.call(args) do |reply|
641
738
  if get.size > 1 && !store
642
- if reply
643
- reply.each_slice(get.size).to_a
644
- end
739
+ reply.each_slice(get.size).to_a if reply
645
740
  else
646
741
  reply
647
742
  end
@@ -666,7 +761,7 @@ class Redis
666
761
  # # => 4
667
762
  #
668
763
  # @param [String] key
669
- # @return [Fixnum] value after decrementing it
764
+ # @return [Integer] value after decrementing it
670
765
  def decr(key)
671
766
  synchronize do |client|
672
767
  client.call([:decr, key])
@@ -680,8 +775,8 @@ class Redis
680
775
  # # => 0
681
776
  #
682
777
  # @param [String] key
683
- # @param [Fixnum] decrement
684
- # @return [Fixnum] value after decrementing it
778
+ # @param [Integer] decrement
779
+ # @return [Integer] value after decrementing it
685
780
  def decrby(key, decrement)
686
781
  synchronize do |client|
687
782
  client.call([:decrby, key, decrement])
@@ -695,7 +790,7 @@ class Redis
695
790
  # # => 6
696
791
  #
697
792
  # @param [String] key
698
- # @return [Fixnum] value after incrementing it
793
+ # @return [Integer] value after incrementing it
699
794
  def incr(key)
700
795
  synchronize do |client|
701
796
  client.call([:incr, key])
@@ -709,8 +804,8 @@ class Redis
709
804
  # # => 10
710
805
  #
711
806
  # @param [String] key
712
- # @param [Fixnum] increment
713
- # @return [Fixnum] value after incrementing it
807
+ # @param [Integer] increment
808
+ # @return [Integer] value after incrementing it
714
809
  def incrby(key, increment)
715
810
  synchronize do |client|
716
811
  client.call([:incrby, key, increment])
@@ -737,31 +832,25 @@ class Redis
737
832
  # @param [String] key
738
833
  # @param [String] value
739
834
  # @param [Hash] options
740
- # - `:ex => Fixnum`: Set the specified expire time, in seconds.
741
- # - `:px => Fixnum`: Set the specified expire time, in milliseconds.
835
+ # - `:ex => Integer`: Set the specified expire time, in seconds.
836
+ # - `:px => Integer`: Set the specified expire time, in milliseconds.
742
837
  # - `:nx => true`: Only set the key if it does not already exist.
743
838
  # - `:xx => true`: Only set the key if it already exist.
839
+ # - `:keepttl => true`: Retain the time to live associated with the key.
744
840
  # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
745
- def set(key, value, options = {})
746
- args = []
747
-
748
- ex = options[:ex]
749
- args.concat(["EX", ex]) if ex
750
-
751
- px = options[:px]
752
- args.concat(["PX", px]) if px
753
-
754
- nx = options[:nx]
755
- args.concat(["NX"]) if nx
756
-
757
- xx = options[:xx]
758
- 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
759
848
 
760
849
  synchronize do |client|
761
850
  if nx || xx
762
- client.call([:set, key, value.to_s] + args, &BoolifySet)
851
+ client.call(args, &BoolifySet)
763
852
  else
764
- client.call([:set, key, value.to_s] + args)
853
+ client.call(args)
765
854
  end
766
855
  end
767
856
  end
@@ -769,7 +858,7 @@ class Redis
769
858
  # Set the time to live in seconds of a key.
770
859
  #
771
860
  # @param [String] key
772
- # @param [Fixnum] ttl
861
+ # @param [Integer] ttl
773
862
  # @param [String] value
774
863
  # @return [String] `"OK"`
775
864
  def setex(key, ttl, value)
@@ -781,7 +870,7 @@ class Redis
781
870
  # Set the time to live in milliseconds of a key.
782
871
  #
783
872
  # @param [String] key
784
- # @param [Fixnum] ttl
873
+ # @param [Integer] ttl
785
874
  # @param [String] value
786
875
  # @return [String] `"OK"`
787
876
  def psetex(key, ttl, value)
@@ -843,7 +932,7 @@ class Redis
843
932
  # @see #mapped_msetnx
844
933
  def msetnx(*args)
845
934
  synchronize do |client|
846
- client.call([:msetnx] + args, &Boolify)
935
+ client.call([:msetnx, *args], &Boolify)
847
936
  end
848
937
  end
849
938
 
@@ -874,7 +963,7 @@ class Redis
874
963
  # Get the values of all the given keys.
875
964
  #
876
965
  # @example
877
- # redis.mget("key1", "key1")
966
+ # redis.mget("key1", "key2")
878
967
  # # => ["v1", "v2"]
879
968
  #
880
969
  # @param [Array<String>] keys
@@ -883,7 +972,7 @@ class Redis
883
972
  # @see #mapped_mget
884
973
  def mget(*keys, &blk)
885
974
  synchronize do |client|
886
- client.call([:mget] + keys, &blk)
975
+ client.call([:mget, *keys], &blk)
887
976
  end
888
977
  end
889
978
 
@@ -899,7 +988,7 @@ class Redis
899
988
  # @see #mget
900
989
  def mapped_mget(*keys)
901
990
  mget(*keys) do |reply|
902
- if reply.kind_of?(Array)
991
+ if reply.is_a?(Array)
903
992
  Hash[keys.zip(reply)]
904
993
  else
905
994
  reply
@@ -910,9 +999,9 @@ class Redis
910
999
  # Overwrite part of a string at key starting at the specified offset.
911
1000
  #
912
1001
  # @param [String] key
913
- # @param [Fixnum] offset byte offset
1002
+ # @param [Integer] offset byte offset
914
1003
  # @param [String] value
915
- # @return [Fixnum] length of the string after it was modified
1004
+ # @return [Integer] length of the string after it was modified
916
1005
  def setrange(key, offset, value)
917
1006
  synchronize do |client|
918
1007
  client.call([:setrange, key, offset, value.to_s])
@@ -922,10 +1011,10 @@ class Redis
922
1011
  # Get a substring of the string stored at a key.
923
1012
  #
924
1013
  # @param [String] key
925
- # @param [Fixnum] start zero-based start offset
926
- # @param [Fixnum] stop zero-based end offset. Use -1 for representing
1014
+ # @param [Integer] start zero-based start offset
1015
+ # @param [Integer] stop zero-based end offset. Use -1 for representing
927
1016
  # the end of the string
928
- # @return [Fixnum] `0` or `1`
1017
+ # @return [Integer] `0` or `1`
929
1018
  def getrange(key, start, stop)
930
1019
  synchronize do |client|
931
1020
  client.call([:getrange, key, start, stop])
@@ -935,9 +1024,9 @@ class Redis
935
1024
  # Sets or clears the bit at offset in the string value stored at key.
936
1025
  #
937
1026
  # @param [String] key
938
- # @param [Fixnum] offset bit offset
939
- # @param [Fixnum] value bit value `0` or `1`
940
- # @return [Fixnum] the original bit value stored at `offset`
1027
+ # @param [Integer] offset bit offset
1028
+ # @param [Integer] value bit value `0` or `1`
1029
+ # @return [Integer] the original bit value stored at `offset`
941
1030
  def setbit(key, offset, value)
942
1031
  synchronize do |client|
943
1032
  client.call([:setbit, key, offset, value])
@@ -947,8 +1036,8 @@ class Redis
947
1036
  # Returns the bit value at offset in the string value stored at key.
948
1037
  #
949
1038
  # @param [String] key
950
- # @param [Fixnum] offset bit offset
951
- # @return [Fixnum] `0` or `1`
1039
+ # @param [Integer] offset bit offset
1040
+ # @return [Integer] `0` or `1`
952
1041
  def getbit(key, offset)
953
1042
  synchronize do |client|
954
1043
  client.call([:getbit, key, offset])
@@ -959,7 +1048,7 @@ class Redis
959
1048
  #
960
1049
  # @param [String] key
961
1050
  # @param [String] value value to append
962
- # @return [Fixnum] length of the string after appending
1051
+ # @return [Integer] length of the string after appending
963
1052
  def append(key, value)
964
1053
  synchronize do |client|
965
1054
  client.call([:append, key, value])
@@ -969,9 +1058,9 @@ class Redis
969
1058
  # Count the number of set bits in a range of the string value stored at key.
970
1059
  #
971
1060
  # @param [String] key
972
- # @param [Fixnum] start start index
973
- # @param [Fixnum] stop stop index
974
- # @return [Fixnum] the number of bits set to 1
1061
+ # @param [Integer] start start index
1062
+ # @param [Integer] stop stop index
1063
+ # @return [Integer] the number of bits set to 1
975
1064
  def bitcount(key, start = 0, stop = -1)
976
1065
  synchronize do |client|
977
1066
  client.call([:bitcount, key, start, stop])
@@ -983,25 +1072,23 @@ class Redis
983
1072
  # @param [String] operation e.g. `and`, `or`, `xor`, `not`
984
1073
  # @param [String] destkey destination key
985
1074
  # @param [String, Array<String>] keys one or more source keys to perform `operation`
986
- # @return [Fixnum] the length of the string stored in `destkey`
1075
+ # @return [Integer] the length of the string stored in `destkey`
987
1076
  def bitop(operation, destkey, *keys)
988
1077
  synchronize do |client|
989
- client.call([:bitop, operation, destkey] + keys)
1078
+ client.call([:bitop, operation, destkey, *keys])
990
1079
  end
991
1080
  end
992
1081
 
993
1082
  # Return the position of the first bit set to 1 or 0 in a string.
994
1083
  #
995
1084
  # @param [String] key
996
- # @param [Fixnum] bit whether to look for the first 1 or 0 bit
997
- # @param [Fixnum] start start index
998
- # @param [Fixnum] stop stop index
999
- # @return [Fixnum] the position of the first 1/0 bit.
1085
+ # @param [Integer] bit whether to look for the first 1 or 0 bit
1086
+ # @param [Integer] start start index
1087
+ # @param [Integer] stop stop index
1088
+ # @return [Integer] the position of the first 1/0 bit.
1000
1089
  # -1 if looking for 1 and it is not found or start and stop are given.
1001
- def bitpos(key, bit, start=nil, stop=nil)
1002
- if stop and not start
1003
- raise(ArgumentError, 'stop parameter specified without start parameter')
1004
- end
1090
+ def bitpos(key, bit, start = nil, stop = nil)
1091
+ raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
1005
1092
 
1006
1093
  synchronize do |client|
1007
1094
  command = [:bitpos, key, bit]
@@ -1026,7 +1113,7 @@ class Redis
1026
1113
  # Get the length of the value stored in a key.
1027
1114
  #
1028
1115
  # @param [String] key
1029
- # @return [Fixnum] the length of the value stored in the key, or 0
1116
+ # @return [Integer] the length of the value stored in the key, or 0
1030
1117
  # if the key does not exist
1031
1118
  def strlen(key)
1032
1119
  synchronize do |client|
@@ -1037,7 +1124,7 @@ class Redis
1037
1124
  # Get the length of a list.
1038
1125
  #
1039
1126
  # @param [String] key
1040
- # @return [Fixnum]
1127
+ # @return [Integer]
1041
1128
  def llen(key)
1042
1129
  synchronize do |client|
1043
1130
  client.call([:llen, key])
@@ -1047,8 +1134,8 @@ class Redis
1047
1134
  # Prepend one or more values to a list, creating the list if it doesn't exist
1048
1135
  #
1049
1136
  # @param [String] key
1050
- # @param [String, Array] value string value, or array of string values to push
1051
- # @return [Fixnum] the length of the list after the push operation
1137
+ # @param [String, Array<String>] value string value, or array of string values to push
1138
+ # @return [Integer] the length of the list after the push operation
1052
1139
  def lpush(key, value)
1053
1140
  synchronize do |client|
1054
1141
  client.call([:lpush, key, value])
@@ -1059,7 +1146,7 @@ class Redis
1059
1146
  #
1060
1147
  # @param [String] key
1061
1148
  # @param [String] value
1062
- # @return [Fixnum] the length of the list after the push operation
1149
+ # @return [Integer] the length of the list after the push operation
1063
1150
  def lpushx(key, value)
1064
1151
  synchronize do |client|
1065
1152
  client.call([:lpushx, key, value])
@@ -1069,8 +1156,8 @@ class Redis
1069
1156
  # Append one or more values to a list, creating the list if it doesn't exist
1070
1157
  #
1071
1158
  # @param [String] key
1072
- # @param [String] value
1073
- # @return [Fixnum] the length of the list after the push operation
1159
+ # @param [String, Array<String>] value string value, or array of string values to push
1160
+ # @return [Integer] the length of the list after the push operation
1074
1161
  def rpush(key, value)
1075
1162
  synchronize do |client|
1076
1163
  client.call([:rpush, key, value])
@@ -1081,30 +1168,36 @@ class Redis
1081
1168
  #
1082
1169
  # @param [String] key
1083
1170
  # @param [String] value
1084
- # @return [Fixnum] the length of the list after the push operation
1171
+ # @return [Integer] the length of the list after the push operation
1085
1172
  def rpushx(key, value)
1086
1173
  synchronize do |client|
1087
1174
  client.call([:rpushx, key, value])
1088
1175
  end
1089
1176
  end
1090
1177
 
1091
- # Remove and get the first element in a list.
1178
+ # Remove and get the first elements in a list.
1092
1179
  #
1093
1180
  # @param [String] key
1094
- # @return [String]
1095
- 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)
1096
1184
  synchronize do |client|
1097
- client.call([:lpop, key])
1185
+ command = [:lpop, key]
1186
+ command << count if count
1187
+ client.call(command)
1098
1188
  end
1099
1189
  end
1100
1190
 
1101
- # Remove and get the last element in a list.
1191
+ # Remove and get the last elements in a list.
1102
1192
  #
1103
1193
  # @param [String] key
1104
- # @return [String]
1105
- 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)
1106
1197
  synchronize do |client|
1107
- client.call([:rpop, key])
1198
+ command = [:rpop, key]
1199
+ command << count if count
1200
+ client.call(command)
1108
1201
  end
1109
1202
  end
1110
1203
 
@@ -1119,28 +1212,27 @@ class Redis
1119
1212
  end
1120
1213
  end
1121
1214
 
1122
- def _bpop(cmd, args)
1123
- options = {}
1124
-
1125
- case args.last
1126
- when Hash
1215
+ def _bpop(cmd, args, &blk)
1216
+ timeout = if args.last.is_a?(Hash)
1127
1217
  options = args.pop
1128
- when Integer
1218
+ options[:timeout]
1219
+ elsif args.last.respond_to?(:to_int)
1129
1220
  # Issue deprecation notice in obnoxious mode...
1130
- options[:timeout] = args.pop
1221
+ args.pop.to_int
1131
1222
  end
1132
1223
 
1224
+ timeout ||= 0
1225
+
1133
1226
  if args.size > 1
1134
1227
  # Issue deprecation notice in obnoxious mode...
1135
1228
  end
1136
1229
 
1137
1230
  keys = args.flatten
1138
- timeout = options[:timeout] || 0
1139
1231
 
1140
1232
  synchronize do |client|
1141
1233
  command = [cmd, keys, timeout]
1142
1234
  timeout += client.timeout if timeout > 0
1143
- client.call_with_timeout(command, timeout)
1235
+ client.call_with_timeout(command, timeout, &blk)
1144
1236
  end
1145
1237
  end
1146
1238
 
@@ -1160,7 +1252,7 @@ class Redis
1160
1252
  # @param [String, Array<String>] keys one or more keys to perform the
1161
1253
  # blocking pop on
1162
1254
  # @param [Hash] options
1163
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1255
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
1164
1256
  #
1165
1257
  # @return [nil, [String, String]]
1166
1258
  # - `nil` when the operation timed out
@@ -1174,7 +1266,7 @@ class Redis
1174
1266
  # @param [String, Array<String>] keys one or more keys to perform the
1175
1267
  # blocking pop on
1176
1268
  # @param [Hash] options
1177
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1269
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
1178
1270
  #
1179
1271
  # @return [nil, [String, String]]
1180
1272
  # - `nil` when the operation timed out
@@ -1191,20 +1283,12 @@ class Redis
1191
1283
  # @param [String] source source key
1192
1284
  # @param [String] destination destination key
1193
1285
  # @param [Hash] options
1194
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1286
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
1195
1287
  #
1196
1288
  # @return [nil, String]
1197
1289
  # - `nil` when the operation timed out
1198
1290
  # - the element was popped and pushed otherwise
1199
- def brpoplpush(source, destination, options = {})
1200
- case options
1201
- when Integer
1202
- # Issue deprecation notice in obnoxious mode...
1203
- options = { :timeout => options }
1204
- end
1205
-
1206
- timeout = options[:timeout] || 0
1207
-
1291
+ def brpoplpush(source, destination, deprecated_timeout = 0, timeout: deprecated_timeout)
1208
1292
  synchronize do |client|
1209
1293
  command = [:brpoplpush, source, destination, timeout]
1210
1294
  timeout += client.timeout if timeout > 0
@@ -1215,7 +1299,7 @@ class Redis
1215
1299
  # Get an element from a list by its index.
1216
1300
  #
1217
1301
  # @param [String] key
1218
- # @param [Fixnum] index
1302
+ # @param [Integer] index
1219
1303
  # @return [String]
1220
1304
  def lindex(key, index)
1221
1305
  synchronize do |client|
@@ -1229,7 +1313,7 @@ class Redis
1229
1313
  # @param [String, Symbol] where `BEFORE` or `AFTER`
1230
1314
  # @param [String] pivot reference element
1231
1315
  # @param [String] value
1232
- # @return [Fixnum] length of the list after the insert operation, or `-1`
1316
+ # @return [Integer] length of the list after the insert operation, or `-1`
1233
1317
  # when the element `pivot` was not found
1234
1318
  def linsert(key, where, pivot, value)
1235
1319
  synchronize do |client|
@@ -1240,8 +1324,8 @@ class Redis
1240
1324
  # Get a range of elements from a list.
1241
1325
  #
1242
1326
  # @param [String] key
1243
- # @param [Fixnum] start start index
1244
- # @param [Fixnum] stop stop index
1327
+ # @param [Integer] start start index
1328
+ # @param [Integer] stop stop index
1245
1329
  # @return [Array<String>]
1246
1330
  def lrange(key, start, stop)
1247
1331
  synchronize do |client|
@@ -1252,12 +1336,12 @@ class Redis
1252
1336
  # Remove elements from a list.
1253
1337
  #
1254
1338
  # @param [String] key
1255
- # @param [Fixnum] count number of elements to remove. Use a positive
1339
+ # @param [Integer] count number of elements to remove. Use a positive
1256
1340
  # value to remove the first `count` occurrences of `value`. A negative
1257
1341
  # value to remove the last `count` occurrences of `value`. Or zero, to
1258
1342
  # remove all occurrences of `value` from the list.
1259
1343
  # @param [String] value
1260
- # @return [Fixnum] the number of removed elements
1344
+ # @return [Integer] the number of removed elements
1261
1345
  def lrem(key, count, value)
1262
1346
  synchronize do |client|
1263
1347
  client.call([:lrem, key, count, value])
@@ -1267,7 +1351,7 @@ class Redis
1267
1351
  # Set the value of an element in a list by its index.
1268
1352
  #
1269
1353
  # @param [String] key
1270
- # @param [Fixnum] index
1354
+ # @param [Integer] index
1271
1355
  # @param [String] value
1272
1356
  # @return [String] `OK`
1273
1357
  def lset(key, index, value)
@@ -1279,8 +1363,8 @@ class Redis
1279
1363
  # Trim a list to the specified range.
1280
1364
  #
1281
1365
  # @param [String] key
1282
- # @param [Fixnum] start start index
1283
- # @param [Fixnum] stop stop index
1366
+ # @param [Integer] start start index
1367
+ # @param [Integer] stop stop index
1284
1368
  # @return [String] `OK`
1285
1369
  def ltrim(key, start, stop)
1286
1370
  synchronize do |client|
@@ -1291,7 +1375,7 @@ class Redis
1291
1375
  # Get the number of members in a set.
1292
1376
  #
1293
1377
  # @param [String] key
1294
- # @return [Fixnum]
1378
+ # @return [Integer]
1295
1379
  def scard(key)
1296
1380
  synchronize do |client|
1297
1381
  client.call([:scard, key])
@@ -1302,8 +1386,8 @@ class Redis
1302
1386
  #
1303
1387
  # @param [String] key
1304
1388
  # @param [String, Array<String>] member one member, or array of members
1305
- # @return [Boolean, Fixnum] `Boolean` when a single member is specified,
1306
- # holding whether or not adding the member succeeded, or `Fixnum` when an
1389
+ # @return [Boolean, Integer] `Boolean` when a single member is specified,
1390
+ # holding whether or not adding the member succeeded, or `Integer` when an
1307
1391
  # array of members is specified, holding the number of members that were
1308
1392
  # successfully added
1309
1393
  def sadd(key, member)
@@ -1324,8 +1408,8 @@ class Redis
1324
1408
  #
1325
1409
  # @param [String] key
1326
1410
  # @param [String, Array<String>] member one member, or array of members
1327
- # @return [Boolean, Fixnum] `Boolean` when a single member is specified,
1328
- # holding whether or not removing the member succeeded, or `Fixnum` when an
1411
+ # @return [Boolean, Integer] `Boolean` when a single member is specified,
1412
+ # holding whether or not removing the member succeeded, or `Integer` when an
1329
1413
  # array of members is specified, holding the number of members that were
1330
1414
  # successfully removed
1331
1415
  def srem(key, member)
@@ -1346,7 +1430,7 @@ class Redis
1346
1430
  #
1347
1431
  # @param [String] key
1348
1432
  # @return [String]
1349
- # @param [Fixnum] count
1433
+ # @param [Integer] count
1350
1434
  def spop(key, count = nil)
1351
1435
  synchronize do |client|
1352
1436
  if count.nil?
@@ -1360,7 +1444,7 @@ class Redis
1360
1444
  # Get one or more random members from a set.
1361
1445
  #
1362
1446
  # @param [String] key
1363
- # @param [Fixnum] count
1447
+ # @param [Integer] count
1364
1448
  # @return [String]
1365
1449
  def srandmember(key, count = nil)
1366
1450
  synchronize do |client|
@@ -1411,7 +1495,7 @@ class Redis
1411
1495
  # @return [Array<String>] members in the difference
1412
1496
  def sdiff(*keys)
1413
1497
  synchronize do |client|
1414
- client.call([:sdiff] + keys)
1498
+ client.call([:sdiff, *keys])
1415
1499
  end
1416
1500
  end
1417
1501
 
@@ -1419,10 +1503,10 @@ class Redis
1419
1503
  #
1420
1504
  # @param [String] destination destination key
1421
1505
  # @param [String, Array<String>] keys keys pointing to sets to subtract
1422
- # @return [Fixnum] number of elements in the resulting set
1506
+ # @return [Integer] number of elements in the resulting set
1423
1507
  def sdiffstore(destination, *keys)
1424
1508
  synchronize do |client|
1425
- client.call([:sdiffstore, destination] + keys)
1509
+ client.call([:sdiffstore, destination, *keys])
1426
1510
  end
1427
1511
  end
1428
1512
 
@@ -1432,7 +1516,7 @@ class Redis
1432
1516
  # @return [Array<String>] members in the intersection
1433
1517
  def sinter(*keys)
1434
1518
  synchronize do |client|
1435
- client.call([:sinter] + keys)
1519
+ client.call([:sinter, *keys])
1436
1520
  end
1437
1521
  end
1438
1522
 
@@ -1440,10 +1524,10 @@ class Redis
1440
1524
  #
1441
1525
  # @param [String] destination destination key
1442
1526
  # @param [String, Array<String>] keys keys pointing to sets to intersect
1443
- # @return [Fixnum] number of elements in the resulting set
1527
+ # @return [Integer] number of elements in the resulting set
1444
1528
  def sinterstore(destination, *keys)
1445
1529
  synchronize do |client|
1446
- client.call([:sinterstore, destination] + keys)
1530
+ client.call([:sinterstore, destination, *keys])
1447
1531
  end
1448
1532
  end
1449
1533
 
@@ -1453,7 +1537,7 @@ class Redis
1453
1537
  # @return [Array<String>] members in the union
1454
1538
  def sunion(*keys)
1455
1539
  synchronize do |client|
1456
- client.call([:sunion] + keys)
1540
+ client.call([:sunion, *keys])
1457
1541
  end
1458
1542
  end
1459
1543
 
@@ -1461,10 +1545,10 @@ class Redis
1461
1545
  #
1462
1546
  # @param [String] destination destination key
1463
1547
  # @param [String, Array<String>] keys keys pointing to sets to unify
1464
- # @return [Fixnum] number of elements in the resulting set
1548
+ # @return [Integer] number of elements in the resulting set
1465
1549
  def sunionstore(destination, *keys)
1466
1550
  synchronize do |client|
1467
- client.call([:sunionstore, destination] + keys)
1551
+ client.call([:sunionstore, destination, *keys])
1468
1552
  end
1469
1553
  end
1470
1554
 
@@ -1475,7 +1559,7 @@ class Redis
1475
1559
  # # => 4
1476
1560
  #
1477
1561
  # @param [String] key
1478
- # @return [Fixnum]
1562
+ # @return [Integer]
1479
1563
  def zcard(key)
1480
1564
  synchronize do |client|
1481
1565
  client.call([:zcard, key])
@@ -1506,38 +1590,27 @@ class Redis
1506
1590
  # - `:incr => true`: When this option is specified ZADD acts like
1507
1591
  # ZINCRBY; only one score-element pair can be specified in this mode
1508
1592
  #
1509
- # @return [Boolean, Fixnum, Float]
1593
+ # @return [Boolean, Integer, Float]
1510
1594
  # - `Boolean` when a single pair is specified, holding whether or not it was
1511
1595
  # **added** to the sorted set.
1512
- # - `Fixnum` when an array of pairs is specified, holding the number of
1596
+ # - `Integer` when an array of pairs is specified, holding the number of
1513
1597
  # pairs that were **added** to the sorted set.
1514
1598
  # - `Float` when option :incr is specified, holding the score of the member
1515
1599
  # after incrementing it.
1516
- def zadd(key, *args) #, options
1517
- zadd_options = []
1518
- if args.last.is_a?(Hash)
1519
- options = args.pop
1520
-
1521
- nx = options[:nx]
1522
- zadd_options << "NX" if nx
1523
-
1524
- xx = options[:xx]
1525
- zadd_options << "XX" if xx
1526
-
1527
- ch = options[:ch]
1528
- zadd_options << "CH" if ch
1529
-
1530
- incr = options[:incr]
1531
- zadd_options << "INCR" if incr
1532
- 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
1533
1606
 
1534
1607
  synchronize do |client|
1535
1608
  if args.size == 1 && args[0].is_a?(Array)
1536
1609
  # Variadic: return float if INCR, integer if !INCR
1537
- client.call([:zadd, key] + zadd_options + args[0], &(incr ? Floatify : nil))
1610
+ client.call(command + args[0], &(incr ? Floatify : nil))
1538
1611
  elsif args.size == 2
1539
1612
  # Single pair: return float if INCR, boolean if !INCR
1540
- client.call([:zadd, key] + zadd_options + args, &(incr ? Floatify : Boolify))
1613
+ client.call(command + args, &(incr ? Floatify : Boolify))
1541
1614
  else
1542
1615
  raise ArgumentError, "wrong number of arguments"
1543
1616
  end
@@ -1572,10 +1645,10 @@ class Redis
1572
1645
  # - a single member
1573
1646
  # - an array of members
1574
1647
  #
1575
- # @return [Boolean, Fixnum]
1648
+ # @return [Boolean, Integer]
1576
1649
  # - `Boolean` when a single member is specified, holding whether or not it
1577
1650
  # was removed from the sorted set
1578
- # - `Fixnum` when an array of pairs is specified, holding the number of
1651
+ # - `Integer` when an array of pairs is specified, holding the number of
1579
1652
  # members that were removed to the sorted set
1580
1653
  def zrem(key, member)
1581
1654
  synchronize do |client|
@@ -1591,6 +1664,90 @@ class Redis
1591
1664
  end
1592
1665
  end
1593
1666
 
1667
+ # Removes and returns up to count members with the highest scores in the sorted set stored at key.
1668
+ #
1669
+ # @example Popping a member
1670
+ # redis.zpopmax('zset')
1671
+ # #=> ['b', 2.0]
1672
+ # @example With count option
1673
+ # redis.zpopmax('zset', 2)
1674
+ # #=> [['b', 2.0], ['a', 1.0]]
1675
+ #
1676
+ # @params key [String] a key of the sorted set
1677
+ # @params count [Integer] a number of members
1678
+ #
1679
+ # @return [Array<String, Float>] element and score pair if count is not specified
1680
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1681
+ def zpopmax(key, count = nil)
1682
+ synchronize do |client|
1683
+ members = client.call([:zpopmax, key, count].compact, &FloatifyPairs)
1684
+ count.to_i > 1 ? members : members.first
1685
+ end
1686
+ end
1687
+
1688
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at key.
1689
+ #
1690
+ # @example Popping a member
1691
+ # redis.zpopmin('zset')
1692
+ # #=> ['a', 1.0]
1693
+ # @example With count option
1694
+ # redis.zpopmin('zset', 2)
1695
+ # #=> [['a', 1.0], ['b', 2.0]]
1696
+ #
1697
+ # @params key [String] a key of the sorted set
1698
+ # @params count [Integer] a number of members
1699
+ #
1700
+ # @return [Array<String, Float>] element and score pair if count is not specified
1701
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1702
+ def zpopmin(key, count = nil)
1703
+ synchronize do |client|
1704
+ members = client.call([:zpopmin, key, count].compact, &FloatifyPairs)
1705
+ count.to_i > 1 ? members : members.first
1706
+ end
1707
+ end
1708
+
1709
+ # Removes and returns up to count members with the highest scores in the sorted set stored at keys,
1710
+ # or block until one is available.
1711
+ #
1712
+ # @example Popping a member from a sorted set
1713
+ # redis.bzpopmax('zset', 1)
1714
+ # #=> ['zset', 'b', 2.0]
1715
+ # @example Popping a member from multiple sorted sets
1716
+ # redis.bzpopmax('zset1', 'zset2', 1)
1717
+ # #=> ['zset1', 'b', 2.0]
1718
+ #
1719
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1720
+ # @params timeout [Integer] the maximum number of seconds to block
1721
+ #
1722
+ # @return [Array<String, String, Float>] a touple of key, member and score
1723
+ # @return [nil] when no element could be popped and the timeout expired
1724
+ def bzpopmax(*args)
1725
+ _bpop(:bzpopmax, args) do |reply|
1726
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1727
+ end
1728
+ end
1729
+
1730
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at keys,
1731
+ # or block until one is available.
1732
+ #
1733
+ # @example Popping a member from a sorted set
1734
+ # redis.bzpopmin('zset', 1)
1735
+ # #=> ['zset', 'a', 1.0]
1736
+ # @example Popping a member from multiple sorted sets
1737
+ # redis.bzpopmin('zset1', 'zset2', 1)
1738
+ # #=> ['zset1', 'a', 1.0]
1739
+ #
1740
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1741
+ # @params timeout [Integer] the maximum number of seconds to block
1742
+ #
1743
+ # @return [Array<String, String, Float>] a touple of key, member and score
1744
+ # @return [nil] when no element could be popped and the timeout expired
1745
+ def bzpopmin(*args)
1746
+ _bpop(:bzpopmin, args) do |reply|
1747
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1748
+ end
1749
+ end
1750
+
1594
1751
  # Get the score associated with the given member in a sorted set.
1595
1752
  #
1596
1753
  # @example Get the score for member "a"
@@ -1616,18 +1773,16 @@ class Redis
1616
1773
  # # => [["a", 32.0], ["b", 64.0]]
1617
1774
  #
1618
1775
  # @param [String] key
1619
- # @param [Fixnum] start start index
1620
- # @param [Fixnum] stop stop index
1776
+ # @param [Integer] start start index
1777
+ # @param [Integer] stop stop index
1621
1778
  # @param [Hash] options
1622
1779
  # - `:with_scores => true`: include scores in output
1623
1780
  #
1624
1781
  # @return [Array<String>, Array<[String, Float]>]
1625
1782
  # - when `:with_scores` is not specified, an array of members
1626
1783
  # - when `:with_scores` is specified, an array with `[member, score]` pairs
1627
- def zrange(key, start, stop, options = {})
1628
- args = []
1629
-
1630
- with_scores = options[:with_scores] || options[:withscores]
1784
+ def zrange(key, start, stop, withscores: false, with_scores: withscores)
1785
+ args = [:zrange, key, start, stop]
1631
1786
 
1632
1787
  if with_scores
1633
1788
  args << "WITHSCORES"
@@ -1635,7 +1790,7 @@ class Redis
1635
1790
  end
1636
1791
 
1637
1792
  synchronize do |client|
1638
- client.call([:zrange, key, start, stop] + args, &block)
1793
+ client.call(args, &block)
1639
1794
  end
1640
1795
  end
1641
1796
 
@@ -1650,10 +1805,8 @@ class Redis
1650
1805
  # # => [["b", 64.0], ["a", 32.0]]
1651
1806
  #
1652
1807
  # @see #zrange
1653
- def zrevrange(key, start, stop, options = {})
1654
- args = []
1655
-
1656
- with_scores = options[:with_scores] || options[:withscores]
1808
+ def zrevrange(key, start, stop, withscores: false, with_scores: withscores)
1809
+ args = [:zrevrange, key, start, stop]
1657
1810
 
1658
1811
  if with_scores
1659
1812
  args << "WITHSCORES"
@@ -1661,7 +1814,7 @@ class Redis
1661
1814
  end
1662
1815
 
1663
1816
  synchronize do |client|
1664
- client.call([:zrevrange, key, start, stop] + args, &block)
1817
+ client.call(args, &block)
1665
1818
  end
1666
1819
  end
1667
1820
 
@@ -1669,7 +1822,7 @@ class Redis
1669
1822
  #
1670
1823
  # @param [String] key
1671
1824
  # @param [String] member
1672
- # @return [Fixnum]
1825
+ # @return [Integer]
1673
1826
  def zrank(key, member)
1674
1827
  synchronize do |client|
1675
1828
  client.call([:zrank, key, member])
@@ -1681,7 +1834,7 @@ class Redis
1681
1834
  #
1682
1835
  # @param [String] key
1683
1836
  # @param [String] member
1684
- # @return [Fixnum]
1837
+ # @return [Integer]
1685
1838
  def zrevrank(key, member)
1686
1839
  synchronize do |client|
1687
1840
  client.call([:zrevrank, key, member])
@@ -1698,15 +1851,39 @@ class Redis
1698
1851
  # # => 5
1699
1852
  #
1700
1853
  # @param [String] key
1701
- # @param [Fixnum] start start index
1702
- # @param [Fixnum] stop stop index
1703
- # @return [Fixnum] number of members that were removed
1854
+ # @param [Integer] start start index
1855
+ # @param [Integer] stop stop index
1856
+ # @return [Integer] number of members that were removed
1704
1857
  def zremrangebyrank(key, start, stop)
1705
1858
  synchronize do |client|
1706
1859
  client.call([:zremrangebyrank, key, start, stop])
1707
1860
  end
1708
1861
  end
1709
1862
 
1863
+ # Count the members, with the same score in a sorted set, within the given lexicographical range.
1864
+ #
1865
+ # @example Count members matching a
1866
+ # redis.zlexcount("zset", "[a", "[a\xff")
1867
+ # # => 1
1868
+ # @example Count members matching a-z
1869
+ # redis.zlexcount("zset", "[a", "[z\xff")
1870
+ # # => 26
1871
+ #
1872
+ # @param [String] key
1873
+ # @param [String] min
1874
+ # - inclusive minimum is specified by prefixing `(`
1875
+ # - exclusive minimum is specified by prefixing `[`
1876
+ # @param [String] max
1877
+ # - inclusive maximum is specified by prefixing `(`
1878
+ # - exclusive maximum is specified by prefixing `[`
1879
+ #
1880
+ # @return [Integer] number of members within the specified lexicographical range
1881
+ def zlexcount(key, min, max)
1882
+ synchronize do |client|
1883
+ client.call([:zlexcount, key, min, max])
1884
+ end
1885
+ end
1886
+
1710
1887
  # Return a range of members with the same score in a sorted set, by lexicographical ordering
1711
1888
  #
1712
1889
  # @example Retrieve members matching a
@@ -1728,14 +1905,16 @@ class Redis
1728
1905
  # `count` members
1729
1906
  #
1730
1907
  # @return [Array<String>, Array<[String, Float]>]
1731
- def zrangebylex(key, min, max, options = {})
1732
- args = []
1908
+ def zrangebylex(key, min, max, limit: nil)
1909
+ args = [:zrangebylex, key, min, max]
1733
1910
 
1734
- limit = options[:limit]
1735
- args.concat(["LIMIT"] + limit) if limit
1911
+ if limit
1912
+ args << "LIMIT"
1913
+ args.concat(limit)
1914
+ end
1736
1915
 
1737
1916
  synchronize do |client|
1738
- client.call([:zrangebylex, key, min, max] + args)
1917
+ client.call(args)
1739
1918
  end
1740
1919
  end
1741
1920
 
@@ -1750,14 +1929,16 @@ class Redis
1750
1929
  # # => ["abbygail", "abby"]
1751
1930
  #
1752
1931
  # @see #zrangebylex
1753
- def zrevrangebylex(key, max, min, options = {})
1754
- args = []
1932
+ def zrevrangebylex(key, max, min, limit: nil)
1933
+ args = [:zrevrangebylex, key, max, min]
1755
1934
 
1756
- limit = options[:limit]
1757
- args.concat(["LIMIT"] + limit) if limit
1935
+ if limit
1936
+ args << "LIMIT"
1937
+ args.concat(limit)
1938
+ end
1758
1939
 
1759
1940
  synchronize do |client|
1760
- client.call([:zrevrangebylex, key, max, min] + args)
1941
+ client.call(args)
1761
1942
  end
1762
1943
  end
1763
1944
 
@@ -1788,21 +1969,21 @@ class Redis
1788
1969
  # @return [Array<String>, Array<[String, Float]>]
1789
1970
  # - when `:with_scores` is not specified, an array of members
1790
1971
  # - when `:with_scores` is specified, an array with `[member, score]` pairs
1791
- def zrangebyscore(key, min, max, options = {})
1792
- args = []
1793
-
1794
- 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]
1795
1974
 
1796
1975
  if with_scores
1797
1976
  args << "WITHSCORES"
1798
1977
  block = FloatifyPairs
1799
1978
  end
1800
1979
 
1801
- limit = options[:limit]
1802
- args.concat(["LIMIT"] + limit) if limit
1980
+ if limit
1981
+ args << "LIMIT"
1982
+ args.concat(limit)
1983
+ end
1803
1984
 
1804
1985
  synchronize do |client|
1805
- client.call([:zrangebyscore, key, min, max] + args, &block)
1986
+ client.call(args, &block)
1806
1987
  end
1807
1988
  end
1808
1989
 
@@ -1820,21 +2001,21 @@ class Redis
1820
2001
  # # => [["b", 64.0], ["a", 32.0]]
1821
2002
  #
1822
2003
  # @see #zrangebyscore
1823
- def zrevrangebyscore(key, max, min, options = {})
1824
- args = []
1825
-
1826
- 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]
1827
2006
 
1828
2007
  if with_scores
1829
- args << ["WITHSCORES"]
2008
+ args << "WITHSCORES"
1830
2009
  block = FloatifyPairs
1831
2010
  end
1832
2011
 
1833
- limit = options[:limit]
1834
- args.concat(["LIMIT"] + limit) if limit
2012
+ if limit
2013
+ args << "LIMIT"
2014
+ args.concat(limit)
2015
+ end
1835
2016
 
1836
2017
  synchronize do |client|
1837
- client.call([:zrevrangebyscore, key, max, min] + args, &block)
2018
+ client.call(args, &block)
1838
2019
  end
1839
2020
  end
1840
2021
 
@@ -1854,7 +2035,7 @@ class Redis
1854
2035
  # @param [String] max
1855
2036
  # - inclusive maximum score is specified verbatim
1856
2037
  # - exclusive maximum score is specified by prefixing `(`
1857
- # @return [Fixnum] number of members that were removed
2038
+ # @return [Integer] number of members that were removed
1858
2039
  def zremrangebyscore(key, min, max)
1859
2040
  synchronize do |client|
1860
2041
  client.call([:zremrangebyscore, key, min, max])
@@ -1877,13 +2058,52 @@ class Redis
1877
2058
  # @param [String] max
1878
2059
  # - inclusive maximum score is specified verbatim
1879
2060
  # - exclusive maximum score is specified by prefixing `(`
1880
- # @return [Fixnum] number of members in within the specified range
2061
+ # @return [Integer] number of members in within the specified range
1881
2062
  def zcount(key, min, max)
1882
2063
  synchronize do |client|
1883
2064
  client.call([:zcount, key, min, max])
1884
2065
  end
1885
2066
  end
1886
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
+
1887
2107
  # Intersect multiple sorted sets and store the resulting sorted set in a new
1888
2108
  # key.
1889
2109
  #
@@ -1897,18 +2117,19 @@ class Redis
1897
2117
  # - `:weights => [Float, Float, ...]`: weights to associate with source
1898
2118
  # sorted sets
1899
2119
  # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
1900
- # @return [Fixnum] number of elements in the resulting sorted set
1901
- def zinterstore(destination, keys, options = {})
1902
- args = []
2120
+ # @return [Integer] number of elements in the resulting sorted set
2121
+ def zinterstore(destination, keys, weights: nil, aggregate: nil)
2122
+ args = [:zinterstore, destination, keys.size, *keys]
1903
2123
 
1904
- weights = options[:weights]
1905
- args.concat(["WEIGHTS"] + weights) if weights
2124
+ if weights
2125
+ args << "WEIGHTS"
2126
+ args.concat(weights)
2127
+ end
1906
2128
 
1907
- aggregate = options[:aggregate]
1908
- args.concat(["AGGREGATE", aggregate]) if aggregate
2129
+ args << "AGGREGATE" << aggregate if aggregate
1909
2130
 
1910
2131
  synchronize do |client|
1911
- client.call([:zinterstore, destination, keys.size] + keys + args)
2132
+ client.call(args)
1912
2133
  end
1913
2134
  end
1914
2135
 
@@ -1924,40 +2145,46 @@ class Redis
1924
2145
  # - `:weights => [Float, Float, ...]`: weights to associate with source
1925
2146
  # sorted sets
1926
2147
  # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
1927
- # @return [Fixnum] number of elements in the resulting sorted set
1928
- def zunionstore(destination, keys, options = {})
1929
- args = []
2148
+ # @return [Integer] number of elements in the resulting sorted set
2149
+ def zunionstore(destination, keys, weights: nil, aggregate: nil)
2150
+ args = [:zunionstore, destination, keys.size, *keys]
1930
2151
 
1931
- weights = options[:weights]
1932
- args.concat(["WEIGHTS"] + weights) if weights
2152
+ if weights
2153
+ args << "WEIGHTS"
2154
+ args.concat(weights)
2155
+ end
1933
2156
 
1934
- aggregate = options[:aggregate]
1935
- args.concat(["AGGREGATE", aggregate]) if aggregate
2157
+ args << "AGGREGATE" << aggregate if aggregate
1936
2158
 
1937
2159
  synchronize do |client|
1938
- client.call([:zunionstore, destination, keys.size] + keys + args)
2160
+ client.call(args)
1939
2161
  end
1940
2162
  end
1941
2163
 
1942
2164
  # Get the number of fields in a hash.
1943
2165
  #
1944
2166
  # @param [String] key
1945
- # @return [Fixnum] number of fields in the hash
2167
+ # @return [Integer] number of fields in the hash
1946
2168
  def hlen(key)
1947
2169
  synchronize do |client|
1948
2170
  client.call([:hlen, key])
1949
2171
  end
1950
2172
  end
1951
2173
 
1952
- # 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
1953
2179
  #
1954
2180
  # @param [String] key
1955
- # @param [String] field
1956
- # @param [String] value
1957
- # @return [Boolean] whether or not the field was **added** to the hash
1958
- 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
+
1959
2186
  synchronize do |client|
1960
- client.call([:hset, key, field, value], &Boolify)
2187
+ client.call([:hset, key, *attrs])
1961
2188
  end
1962
2189
  end
1963
2190
 
@@ -2046,7 +2273,7 @@ class Redis
2046
2273
  # @see #hmget
2047
2274
  def mapped_hmget(key, *fields)
2048
2275
  hmget(key, *fields) do |reply|
2049
- if reply.kind_of?(Array)
2276
+ if reply.is_a?(Array)
2050
2277
  Hash[fields.zip(reply)]
2051
2278
  else
2052
2279
  reply
@@ -2058,10 +2285,10 @@ class Redis
2058
2285
  #
2059
2286
  # @param [String] key
2060
2287
  # @param [String, Array<String>] field
2061
- # @return [Fixnum] the number of fields that were removed from the hash
2062
- def hdel(key, field)
2288
+ # @return [Integer] the number of fields that were removed from the hash
2289
+ def hdel(key, *fields)
2063
2290
  synchronize do |client|
2064
- client.call([:hdel, key, field])
2291
+ client.call([:hdel, key, *fields])
2065
2292
  end
2066
2293
  end
2067
2294
 
@@ -2080,8 +2307,8 @@ class Redis
2080
2307
  #
2081
2308
  # @param [String] key
2082
2309
  # @param [String] field
2083
- # @param [Fixnum] increment
2084
- # @return [Fixnum] value of the field after incrementing it
2310
+ # @param [Integer] increment
2311
+ # @return [Integer] value of the field after incrementing it
2085
2312
  def hincrby(key, field, increment)
2086
2313
  synchronize do |client|
2087
2314
  client.call([:hincrby, key, field, increment])
@@ -2139,20 +2366,21 @@ class Redis
2139
2366
 
2140
2367
  def subscribed?
2141
2368
  synchronize do |client|
2142
- client.kind_of? SubscribedClient
2369
+ client.is_a? SubscribedClient
2143
2370
  end
2144
2371
  end
2145
2372
 
2146
2373
  # Listen for messages published to the given channels.
2147
2374
  def subscribe(*channels, &block)
2148
- synchronize do |client|
2375
+ synchronize do |_client|
2149
2376
  _subscription(:subscribe, 0, channels, block)
2150
2377
  end
2151
2378
  end
2152
2379
 
2153
- # 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.
2154
2382
  def subscribe_with_timeout(timeout, *channels, &block)
2155
- synchronize do |client|
2383
+ synchronize do |_client|
2156
2384
  _subscription(:subscribe_with_timeout, timeout, channels, block)
2157
2385
  end
2158
2386
  end
@@ -2160,21 +2388,23 @@ class Redis
2160
2388
  # Stop listening for messages posted to the given channels.
2161
2389
  def unsubscribe(*channels)
2162
2390
  synchronize do |client|
2163
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2391
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2392
+
2164
2393
  client.unsubscribe(*channels)
2165
2394
  end
2166
2395
  end
2167
2396
 
2168
2397
  # Listen for messages published to channels matching the given patterns.
2169
2398
  def psubscribe(*channels, &block)
2170
- synchronize do |client|
2399
+ synchronize do |_client|
2171
2400
  _subscription(:psubscribe, 0, channels, block)
2172
2401
  end
2173
2402
  end
2174
2403
 
2175
- # 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.
2176
2406
  def psubscribe_with_timeout(timeout, *channels, &block)
2177
- synchronize do |client|
2407
+ synchronize do |_client|
2178
2408
  _subscription(:psubscribe_with_timeout, timeout, channels, block)
2179
2409
  end
2180
2410
  end
@@ -2182,7 +2412,8 @@ class Redis
2182
2412
  # Stop listening for messages posted to channels matching the given patterns.
2183
2413
  def punsubscribe(*channels)
2184
2414
  synchronize do |client|
2185
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2415
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2416
+
2186
2417
  client.punsubscribe(*channels)
2187
2418
  end
2188
2419
  end
@@ -2227,7 +2458,7 @@ class Redis
2227
2458
  # @see #multi
2228
2459
  def watch(*keys)
2229
2460
  synchronize do |client|
2230
- res = client.call([:watch] + keys)
2461
+ res = client.call([:watch, *keys])
2231
2462
 
2232
2463
  if block_given?
2233
2464
  begin
@@ -2257,13 +2488,13 @@ class Redis
2257
2488
  end
2258
2489
 
2259
2490
  def pipelined
2260
- synchronize do |client|
2491
+ synchronize do |prior_client|
2261
2492
  begin
2262
- original, @client = @client, Pipeline.new
2493
+ @client = Pipeline.new(prior_client)
2263
2494
  yield(self)
2264
- original.call_pipeline(@client)
2495
+ prior_client.call_pipeline(@client)
2265
2496
  ensure
2266
- @client = original
2497
+ @client = prior_client
2267
2498
  end
2268
2499
  end
2269
2500
  end
@@ -2299,17 +2530,16 @@ class Redis
2299
2530
  # @see #watch
2300
2531
  # @see #unwatch
2301
2532
  def multi
2302
- synchronize do |client|
2533
+ synchronize do |prior_client|
2303
2534
  if !block_given?
2304
- client.call([:multi])
2535
+ prior_client.call([:multi])
2305
2536
  else
2306
2537
  begin
2307
- pipeline = Pipeline::Multi.new
2308
- original, @client = @client, pipeline
2538
+ @client = Pipeline::Multi.new(prior_client)
2309
2539
  yield(self)
2310
- original.call_pipeline(pipeline)
2540
+ prior_client.call_pipeline(@client)
2311
2541
  ensure
2312
- @client = original
2542
+ @client = prior_client
2313
2543
  end
2314
2544
  end
2315
2545
  end
@@ -2456,18 +2686,13 @@ class Redis
2456
2686
  _eval(:evalsha, args)
2457
2687
  end
2458
2688
 
2459
- def _scan(command, cursor, args, options = {}, &block)
2689
+ def _scan(command, cursor, args, match: nil, count: nil, type: nil, &block)
2460
2690
  # SSCAN/ZSCAN/HSCAN already prepend the key to +args+.
2461
2691
 
2462
2692
  args << cursor
2463
-
2464
- if match = options[:match]
2465
- args.concat(["MATCH", match])
2466
- end
2467
-
2468
- if count = options[:count]
2469
- args.concat(["COUNT", count])
2470
- end
2693
+ args << "MATCH" << match if match
2694
+ args << "COUNT" << count if count
2695
+ args << "TYPE" << type if type
2471
2696
 
2472
2697
  synchronize do |client|
2473
2698
  client.call([command] + args, &block)
@@ -2482,15 +2707,19 @@ class Redis
2482
2707
  # @example Retrieve a batch of keys matching a pattern
2483
2708
  # redis.scan(4, :match => "key:1?")
2484
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"]]
2485
2713
  #
2486
2714
  # @param [String, Integer] cursor the cursor of the iteration
2487
2715
  # @param [Hash] options
2488
2716
  # - `:match => String`: only return keys matching the pattern
2489
2717
  # - `:count => Integer`: return count keys at most per iteration
2718
+ # - `:type => String`: return keys only of the given type
2490
2719
  #
2491
2720
  # @return [String, Array<String>] the next cursor and all found keys
2492
- def scan(cursor, options={})
2493
- _scan(:scan, cursor, [], options)
2721
+ def scan(cursor, **options)
2722
+ _scan(:scan, cursor, [], **options)
2494
2723
  end
2495
2724
 
2496
2725
  # Scan the keyspace
@@ -2502,17 +2731,23 @@ class Redis
2502
2731
  # redis.scan_each(:match => "key:1?") {|key| puts key}
2503
2732
  # # => key:13
2504
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"
2505
2738
  #
2506
2739
  # @param [Hash] options
2507
2740
  # - `:match => String`: only return keys matching the pattern
2508
2741
  # - `:count => Integer`: return count keys at most per iteration
2742
+ # - `:type => String`: return keys only of the given type
2509
2743
  #
2510
2744
  # @return [Enumerator] an enumerator for all found keys
2511
- def scan_each(options={}, &block)
2512
- 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
+
2513
2748
  cursor = 0
2514
2749
  loop do
2515
- cursor, keys = scan(cursor, options)
2750
+ cursor, keys = scan(cursor, **options)
2516
2751
  keys.each(&block)
2517
2752
  break if cursor == "0"
2518
2753
  end
@@ -2529,8 +2764,8 @@ class Redis
2529
2764
  # - `:count => Integer`: return count keys at most per iteration
2530
2765
  #
2531
2766
  # @return [String, Array<[String, String]>] the next cursor and all found keys
2532
- def hscan(key, cursor, options={})
2533
- _scan(:hscan, cursor, [key], options) do |reply|
2767
+ def hscan(key, cursor, **options)
2768
+ _scan(:hscan, cursor, [key], **options) do |reply|
2534
2769
  [reply[0], reply[1].each_slice(2).to_a]
2535
2770
  end
2536
2771
  end
@@ -2546,11 +2781,12 @@ class Redis
2546
2781
  # - `:count => Integer`: return count keys at most per iteration
2547
2782
  #
2548
2783
  # @return [Enumerator] an enumerator for all found keys
2549
- def hscan_each(key, options={}, &block)
2550
- 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
+
2551
2787
  cursor = 0
2552
2788
  loop do
2553
- cursor, values = hscan(key, cursor, options)
2789
+ cursor, values = hscan(key, cursor, **options)
2554
2790
  values.each(&block)
2555
2791
  break if cursor == "0"
2556
2792
  end
@@ -2568,8 +2804,8 @@ class Redis
2568
2804
  #
2569
2805
  # @return [String, Array<[String, Float]>] the next cursor and all found
2570
2806
  # members and scores
2571
- def zscan(key, cursor, options={})
2572
- _scan(:zscan, cursor, [key], options) do |reply|
2807
+ def zscan(key, cursor, **options)
2808
+ _scan(:zscan, cursor, [key], **options) do |reply|
2573
2809
  [reply[0], FloatifyPairs.call(reply[1])]
2574
2810
  end
2575
2811
  end
@@ -2585,11 +2821,12 @@ class Redis
2585
2821
  # - `:count => Integer`: return count keys at most per iteration
2586
2822
  #
2587
2823
  # @return [Enumerator] an enumerator for all found scores and members
2588
- def zscan_each(key, options={}, &block)
2589
- 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
+
2590
2827
  cursor = 0
2591
2828
  loop do
2592
- cursor, values = zscan(key, cursor, options)
2829
+ cursor, values = zscan(key, cursor, **options)
2593
2830
  values.each(&block)
2594
2831
  break if cursor == "0"
2595
2832
  end
@@ -2606,8 +2843,8 @@ class Redis
2606
2843
  # - `:count => Integer`: return count keys at most per iteration
2607
2844
  #
2608
2845
  # @return [String, Array<String>] the next cursor and all found members
2609
- def sscan(key, cursor, options={})
2610
- _scan(:sscan, cursor, [key], options)
2846
+ def sscan(key, cursor, **options)
2847
+ _scan(:sscan, cursor, [key], **options)
2611
2848
  end
2612
2849
 
2613
2850
  # Scan a set
@@ -2621,11 +2858,12 @@ class Redis
2621
2858
  # - `:count => Integer`: return count keys at most per iteration
2622
2859
  #
2623
2860
  # @return [Enumerator] an enumerator for all keys in the set
2624
- def sscan_each(key, options={}, &block)
2625
- 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
+
2626
2864
  cursor = 0
2627
2865
  loop do
2628
- cursor, keys = sscan(key, cursor, options)
2866
+ cursor, keys = sscan(key, cursor, **options)
2629
2867
  keys.each(&block)
2630
2868
  break if cursor == "0"
2631
2869
  end
@@ -2648,7 +2886,7 @@ class Redis
2648
2886
  # union of the HyperLogLogs contained in the keys.
2649
2887
  #
2650
2888
  # @param [String, Array<String>] keys
2651
- # @return [Fixnum]
2889
+ # @return [Integer]
2652
2890
  def pfcount(*keys)
2653
2891
  synchronize do |client|
2654
2892
  client.call([:pfcount] + keys)
@@ -2667,6 +2905,445 @@ class Redis
2667
2905
  end
2668
2906
  end
2669
2907
 
2908
+ # Adds the specified geospatial items (latitude, longitude, name) to the specified key
2909
+ #
2910
+ # @param [String] key
2911
+ # @param [Array] member arguemnts for member or members: longitude, latitude, name
2912
+ # @return [Integer] number of elements added to the sorted set
2913
+ def geoadd(key, *member)
2914
+ synchronize do |client|
2915
+ client.call([:geoadd, key, *member])
2916
+ end
2917
+ end
2918
+
2919
+ # Returns geohash string representing position for specified members of the specified key.
2920
+ #
2921
+ # @param [String] key
2922
+ # @param [String, Array<String>] member one member or array of members
2923
+ # @return [Array<String, nil>] returns array containg geohash string if member is present, nil otherwise
2924
+ def geohash(key, member)
2925
+ synchronize do |client|
2926
+ client.call([:geohash, key, member])
2927
+ end
2928
+ end
2929
+
2930
+ # Query a sorted set representing a geospatial index to fetch members matching a
2931
+ # given maximum distance from a point
2932
+ #
2933
+ # @param [Array] args key, longitude, latitude, radius, unit(m|km|ft|mi)
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
2936
+ # @param [Integer] count limit the results to the first N matching items
2937
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2938
+ # @return [Array<String>] may be changed with `options`
2939
+
2940
+ def georadius(*args, **geoptions)
2941
+ geoarguments = _geoarguments(*args, **geoptions)
2942
+
2943
+ synchronize do |client|
2944
+ client.call([:georadius, *geoarguments])
2945
+ end
2946
+ end
2947
+
2948
+ # Query a sorted set representing a geospatial index to fetch members matching a
2949
+ # given maximum distance from an already existing member
2950
+ #
2951
+ # @param [Array] args key, member, radius, unit(m|km|ft|mi)
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
2954
+ # @param [Integer] count limit the results to the first N matching items
2955
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2956
+ # @return [Array<String>] may be changed with `options`
2957
+
2958
+ def georadiusbymember(*args, **geoptions)
2959
+ geoarguments = _geoarguments(*args, **geoptions)
2960
+
2961
+ synchronize do |client|
2962
+ client.call([:georadiusbymember, *geoarguments])
2963
+ end
2964
+ end
2965
+
2966
+ # Returns longitude and latitude of members of a geospatial index
2967
+ #
2968
+ # @param [String] key
2969
+ # @param [String, Array<String>] member one member or array of members
2970
+ # @return [Array<Array<String>, nil>] returns array of elements, where each
2971
+ # element is either array of longitude and latitude or nil
2972
+ def geopos(key, member)
2973
+ synchronize do |client|
2974
+ client.call([:geopos, key, member])
2975
+ end
2976
+ end
2977
+
2978
+ # Returns the distance between two members of a geospatial index
2979
+ #
2980
+ # @param [String ]key
2981
+ # @param [Array<String>] members
2982
+ # @param ['m', 'km', 'mi', 'ft'] unit
2983
+ # @return [String, nil] returns distance in spefied unit if both members present, nil otherwise.
2984
+ def geodist(key, member1, member2, unit = 'm')
2985
+ synchronize do |client|
2986
+ client.call([:geodist, key, member1, member2, unit])
2987
+ end
2988
+ end
2989
+
2990
+ # Returns the stream information each subcommand.
2991
+ #
2992
+ # @example stream
2993
+ # redis.xinfo(:stream, 'mystream')
2994
+ # @example groups
2995
+ # redis.xinfo(:groups, 'mystream')
2996
+ # @example consumers
2997
+ # redis.xinfo(:consumers, 'mystream', 'mygroup')
2998
+ #
2999
+ # @param subcommand [String] e.g. `stream` `groups` `consumers`
3000
+ # @param key [String] the stream key
3001
+ # @param group [String] the consumer group name, required if subcommand is `consumers`
3002
+ #
3003
+ # @return [Hash] information of the stream if subcommand is `stream`
3004
+ # @return [Array<Hash>] information of the consumer groups if subcommand is `groups`
3005
+ # @return [Array<Hash>] information of the consumers if subcommand is `consumers`
3006
+ def xinfo(subcommand, key, group = nil)
3007
+ args = [:xinfo, subcommand, key, group].compact
3008
+ synchronize do |client|
3009
+ client.call(args) do |reply|
3010
+ case subcommand.to_s.downcase
3011
+ when 'stream' then Hashify.call(reply)
3012
+ when 'groups', 'consumers' then reply.map { |arr| Hashify.call(arr) }
3013
+ else reply
3014
+ end
3015
+ end
3016
+ end
3017
+ end
3018
+
3019
+ # Add new entry to the stream.
3020
+ #
3021
+ # @example Without options
3022
+ # redis.xadd('mystream', f1: 'v1', f2: 'v2')
3023
+ # @example With options
3024
+ # redis.xadd('mystream', { f1: 'v1', f2: 'v2' }, id: '0-0', maxlen: 1000, approximate: true)
3025
+ #
3026
+ # @param key [String] the stream key
3027
+ # @param entry [Hash] one or multiple field-value pairs
3028
+ # @param opts [Hash] several options for `XADD` command
3029
+ #
3030
+ # @option opts [String] :id the entry id, default value is `*`, it means auto generation
3031
+ # @option opts [Integer] :maxlen max length of entries
3032
+ # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen or not
3033
+ #
3034
+ # @return [String] the entry id
3035
+ def xadd(key, entry, approximate: nil, maxlen: nil, id: '*')
3036
+ args = [:xadd, key]
3037
+ if maxlen
3038
+ args << "MAXLEN"
3039
+ args << "~" if approximate
3040
+ args << maxlen
3041
+ end
3042
+ args << id
3043
+ args.concat(entry.to_a.flatten)
3044
+ synchronize { |client| client.call(args) }
3045
+ end
3046
+
3047
+ # Trims older entries of the stream if needed.
3048
+ #
3049
+ # @example Without options
3050
+ # redis.xtrim('mystream', 1000)
3051
+ # @example With options
3052
+ # redis.xtrim('mystream', 1000, approximate: true)
3053
+ #
3054
+ # @param key [String] the stream key
3055
+ # @param mexlen [Integer] max length of entries
3056
+ # @param approximate [Boolean] whether to add `~` modifier of maxlen or not
3057
+ #
3058
+ # @return [Integer] the number of entries actually deleted
3059
+ def xtrim(key, maxlen, approximate: false)
3060
+ args = [:xtrim, key, 'MAXLEN', (approximate ? '~' : nil), maxlen].compact
3061
+ synchronize { |client| client.call(args) }
3062
+ end
3063
+
3064
+ # Delete entries by entry ids.
3065
+ #
3066
+ # @example With splatted entry ids
3067
+ # redis.xdel('mystream', '0-1', '0-2')
3068
+ # @example With arrayed entry ids
3069
+ # redis.xdel('mystream', ['0-1', '0-2'])
3070
+ #
3071
+ # @param key [String] the stream key
3072
+ # @param ids [Array<String>] one or multiple entry ids
3073
+ #
3074
+ # @return [Integer] the number of entries actually deleted
3075
+ def xdel(key, *ids)
3076
+ args = [:xdel, key].concat(ids.flatten)
3077
+ synchronize { |client| client.call(args) }
3078
+ end
3079
+
3080
+ # Fetches entries of the stream in ascending order.
3081
+ #
3082
+ # @example Without options
3083
+ # redis.xrange('mystream')
3084
+ # @example With a specific start
3085
+ # redis.xrange('mystream', '0-1')
3086
+ # @example With a specific start and end
3087
+ # redis.xrange('mystream', '0-1', '0-3')
3088
+ # @example With count options
3089
+ # redis.xrange('mystream', count: 10)
3090
+ #
3091
+ # @param key [String] the stream key
3092
+ # @param start [String] first entry id of range, default value is `-`
3093
+ # @param end [String] last entry id of range, default value is `+`
3094
+ # @param count [Integer] the number of entries as limit
3095
+ #
3096
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3097
+ def xrange(key, start = '-', range_end = '+', count: nil)
3098
+ args = [:xrange, key, start, range_end]
3099
+ args.concat(['COUNT', count]) if count
3100
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3101
+ end
3102
+
3103
+ # Fetches entries of the stream in descending order.
3104
+ #
3105
+ # @example Without options
3106
+ # redis.xrevrange('mystream')
3107
+ # @example With a specific end
3108
+ # redis.xrevrange('mystream', '0-3')
3109
+ # @example With a specific end and start
3110
+ # redis.xrevrange('mystream', '0-3', '0-1')
3111
+ # @example With count options
3112
+ # redis.xrevrange('mystream', count: 10)
3113
+ #
3114
+ # @param key [String] the stream key
3115
+ # @param end [String] first entry id of range, default value is `+`
3116
+ # @param start [String] last entry id of range, default value is `-`
3117
+ # @params count [Integer] the number of entries as limit
3118
+ #
3119
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3120
+ def xrevrange(key, range_end = '+', start = '-', count: nil)
3121
+ args = [:xrevrange, key, range_end, start]
3122
+ args.concat(['COUNT', count]) if count
3123
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3124
+ end
3125
+
3126
+ # Returns the number of entries inside a stream.
3127
+ #
3128
+ # @example With key
3129
+ # redis.xlen('mystream')
3130
+ #
3131
+ # @param key [String] the stream key
3132
+ #
3133
+ # @return [Integer] the number of entries
3134
+ def xlen(key)
3135
+ synchronize { |client| client.call([:xlen, key]) }
3136
+ end
3137
+
3138
+ # Fetches entries from one or multiple streams. Optionally blocking.
3139
+ #
3140
+ # @example With a key
3141
+ # redis.xread('mystream', '0-0')
3142
+ # @example With multiple keys
3143
+ # redis.xread(%w[mystream1 mystream2], %w[0-0 0-0])
3144
+ # @example With count option
3145
+ # redis.xread('mystream', '0-0', count: 2)
3146
+ # @example With block option
3147
+ # redis.xread('mystream', '$', block: 1000)
3148
+ #
3149
+ # @param keys [Array<String>] one or multiple stream keys
3150
+ # @param ids [Array<String>] one or multiple entry ids
3151
+ # @param count [Integer] the number of entries as limit per stream
3152
+ # @param block [Integer] the number of milliseconds as blocking timeout
3153
+ #
3154
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3155
+ def xread(keys, ids, count: nil, block: nil)
3156
+ args = [:xread]
3157
+ args << 'COUNT' << count if count
3158
+ args << 'BLOCK' << block.to_i if block
3159
+ _xread(args, keys, ids, block)
3160
+ end
3161
+
3162
+ # Manages the consumer group of the stream.
3163
+ #
3164
+ # @example With `create` subcommand
3165
+ # redis.xgroup(:create, 'mystream', 'mygroup', '$')
3166
+ # @example With `setid` subcommand
3167
+ # redis.xgroup(:setid, 'mystream', 'mygroup', '$')
3168
+ # @example With `destroy` subcommand
3169
+ # redis.xgroup(:destroy, 'mystream', 'mygroup')
3170
+ # @example With `delconsumer` subcommand
3171
+ # redis.xgroup(:delconsumer, 'mystream', 'mygroup', 'consumer1')
3172
+ #
3173
+ # @param subcommand [String] `create` `setid` `destroy` `delconsumer`
3174
+ # @param key [String] the stream key
3175
+ # @param group [String] the consumer group name
3176
+ # @param id_or_consumer [String]
3177
+ # * the entry id or `$`, required if subcommand is `create` or `setid`
3178
+ # * the consumer name, required if subcommand is `delconsumer`
3179
+ # @param mkstream [Boolean] whether to create an empty stream automatically or not
3180
+ #
3181
+ # @return [String] `OK` if subcommand is `create` or `setid`
3182
+ # @return [Integer] effected count if subcommand is `destroy` or `delconsumer`
3183
+ def xgroup(subcommand, key, group, id_or_consumer = nil, mkstream: false)
3184
+ args = [:xgroup, subcommand, key, group, id_or_consumer, (mkstream ? 'MKSTREAM' : nil)].compact
3185
+ synchronize { |client| client.call(args) }
3186
+ end
3187
+
3188
+ # Fetches a subset of the entries from one or multiple streams related with the consumer group.
3189
+ # Optionally blocking.
3190
+ #
3191
+ # @example With a key
3192
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>')
3193
+ # @example With multiple keys
3194
+ # redis.xreadgroup('mygroup', 'consumer1', %w[mystream1 mystream2], %w[> >])
3195
+ # @example With count option
3196
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', count: 2)
3197
+ # @example With block option
3198
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', block: 1000)
3199
+ # @example With noack option
3200
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', noack: true)
3201
+ #
3202
+ # @param group [String] the consumer group name
3203
+ # @param consumer [String] the consumer name
3204
+ # @param keys [Array<String>] one or multiple stream keys
3205
+ # @param ids [Array<String>] one or multiple entry ids
3206
+ # @param opts [Hash] several options for `XREADGROUP` command
3207
+ #
3208
+ # @option opts [Integer] :count the number of entries as limit
3209
+ # @option opts [Integer] :block the number of milliseconds as blocking timeout
3210
+ # @option opts [Boolean] :noack whether message loss is acceptable or not
3211
+ #
3212
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3213
+ def xreadgroup(group, consumer, keys, ids, count: nil, block: nil, noack: nil)
3214
+ args = [:xreadgroup, 'GROUP', group, consumer]
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)
3219
+ end
3220
+
3221
+ # Removes one or multiple entries from the pending entries list of a stream consumer group.
3222
+ #
3223
+ # @example With a entry id
3224
+ # redis.xack('mystream', 'mygroup', '1526569495631-0')
3225
+ # @example With splatted entry ids
3226
+ # redis.xack('mystream', 'mygroup', '0-1', '0-2')
3227
+ # @example With arrayed entry ids
3228
+ # redis.xack('mystream', 'mygroup', %w[0-1 0-2])
3229
+ #
3230
+ # @param key [String] the stream key
3231
+ # @param group [String] the consumer group name
3232
+ # @param ids [Array<String>] one or multiple entry ids
3233
+ #
3234
+ # @return [Integer] the number of entries successfully acknowledged
3235
+ def xack(key, group, *ids)
3236
+ args = [:xack, key, group].concat(ids.flatten)
3237
+ synchronize { |client| client.call(args) }
3238
+ end
3239
+
3240
+ # Changes the ownership of a pending entry
3241
+ #
3242
+ # @example With splatted entry ids
3243
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-1', '0-2')
3244
+ # @example With arrayed entry ids
3245
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2])
3246
+ # @example With idle option
3247
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], idle: 1000)
3248
+ # @example With time option
3249
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], time: 1542866959000)
3250
+ # @example With retrycount option
3251
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], retrycount: 10)
3252
+ # @example With force option
3253
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], force: true)
3254
+ # @example With justid option
3255
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], justid: true)
3256
+ #
3257
+ # @param key [String] the stream key
3258
+ # @param group [String] the consumer group name
3259
+ # @param consumer [String] the consumer name
3260
+ # @param min_idle_time [Integer] the number of milliseconds
3261
+ # @param ids [Array<String>] one or multiple entry ids
3262
+ # @param opts [Hash] several options for `XCLAIM` command
3263
+ #
3264
+ # @option opts [Integer] :idle the number of milliseconds as last time it was delivered of the entry
3265
+ # @option opts [Integer] :time the number of milliseconds as a specific Unix Epoch time
3266
+ # @option opts [Integer] :retrycount the number of retry counter
3267
+ # @option opts [Boolean] :force whether to create the pending entry to the pending entries list or not
3268
+ # @option opts [Boolean] :justid whether to fetch just an array of entry ids or not
3269
+ #
3270
+ # @return [Hash{String => Hash}] the entries successfully claimed
3271
+ # @return [Array<String>] the entry ids successfully claimed if justid option is `true`
3272
+ def xclaim(key, group, consumer, min_idle_time, *ids, **opts)
3273
+ args = [:xclaim, key, group, consumer, min_idle_time].concat(ids.flatten)
3274
+ args.concat(['IDLE', opts[:idle].to_i]) if opts[:idle]
3275
+ args.concat(['TIME', opts[:time].to_i]) if opts[:time]
3276
+ args.concat(['RETRYCOUNT', opts[:retrycount]]) if opts[:retrycount]
3277
+ args << 'FORCE' if opts[:force]
3278
+ args << 'JUSTID' if opts[:justid]
3279
+ blk = opts[:justid] ? Noop : HashifyStreamEntries
3280
+ synchronize { |client| client.call(args, &blk) }
3281
+ end
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
+
3315
+ # Fetches not acknowledging pending entries
3316
+ #
3317
+ # @example With key and group
3318
+ # redis.xpending('mystream', 'mygroup')
3319
+ # @example With range options
3320
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10)
3321
+ # @example With range and consumer options
3322
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10, 'consumer1')
3323
+ #
3324
+ # @param key [String] the stream key
3325
+ # @param group [String] the consumer group name
3326
+ # @param start [String] start first entry id of range
3327
+ # @param end [String] end last entry id of range
3328
+ # @param count [Integer] count the number of entries as limit
3329
+ # @param consumer [String] the consumer name
3330
+ #
3331
+ # @return [Hash] the summary of pending entries
3332
+ # @return [Array<Hash>] the pending entries details if options were specified
3333
+ def xpending(key, group, *args)
3334
+ command_args = [:xpending, key, group]
3335
+ case args.size
3336
+ when 0, 3, 4
3337
+ command_args.concat(args)
3338
+ else
3339
+ raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 2, 5 or 6)"
3340
+ end
3341
+
3342
+ summary_needed = args.empty?
3343
+ blk = summary_needed ? HashifyStreamPendings : HashifyStreamPendingDetails
3344
+ synchronize { |client| client.call(command_args, &blk) }
3345
+ end
3346
+
2670
3347
  # Interact with the sentinel command (masters, master, slaves, failover)
2671
3348
  #
2672
3349
  # @param [String] subcommand e.g. `masters`, `master`, `slaves`
@@ -2680,8 +3357,8 @@ class Redis
2680
3357
  when "get-master-addr-by-name"
2681
3358
  reply
2682
3359
  else
2683
- if reply.kind_of?(Array)
2684
- if reply[0].kind_of?(Array)
3360
+ if reply.is_a?(Array)
3361
+ if reply[0].is_a?(Array)
2685
3362
  reply.map(&Hashify)
2686
3363
  else
2687
3364
  Hashify.call(reply)
@@ -2694,6 +3371,46 @@ class Redis
2694
3371
  end
2695
3372
  end
2696
3373
 
3374
+ # Sends `CLUSTER *` command to random node and returns its reply.
3375
+ #
3376
+ # @see https://redis.io/commands#cluster Reference of cluster command
3377
+ #
3378
+ # @param subcommand [String, Symbol] the subcommand of cluster command
3379
+ # e.g. `:slots`, `:nodes`, `:slaves`, `:info`
3380
+ #
3381
+ # @return [Object] depends on the subcommand
3382
+ def cluster(subcommand, *args)
3383
+ subcommand = subcommand.to_s.downcase
3384
+ block = case subcommand
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
3396
+
3397
+ # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
3398
+ block = Noop unless @cluster_mode
3399
+
3400
+ synchronize do |client|
3401
+ client.call([:cluster, subcommand] + args, &block)
3402
+ end
3403
+ end
3404
+
3405
+ # Sends `ASKING` command to random node and returns its reply.
3406
+ #
3407
+ # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
3408
+ #
3409
+ # @return [String] `'OK'`
3410
+ def asking
3411
+ synchronize { |client| client.call(%i[asking]) }
3412
+ end
3413
+
2697
3414
  def id
2698
3415
  @original_client.id
2699
3416
  end
@@ -2706,59 +3423,184 @@ class Redis
2706
3423
  self.class.new(@options)
2707
3424
  end
2708
3425
 
2709
- def method_missing(command, *args)
3426
+ def connection
3427
+ return @original_client.connection_info if @cluster_mode
3428
+
3429
+ {
3430
+ host: @original_client.host,
3431
+ port: @original_client.port,
3432
+ db: @original_client.db,
3433
+ id: @original_client.id,
3434
+ location: @original_client.location
3435
+ }
3436
+ end
3437
+
3438
+ def method_missing(command, *args) # rubocop:disable Style/MissingRespondToMissing
2710
3439
  synchronize do |client|
2711
3440
  client.call([command] + args)
2712
3441
  end
2713
3442
  end
2714
3443
 
2715
- private
3444
+ private
2716
3445
 
2717
3446
  # Commands returning 1 for true and 0 for false may be executed in a pipeline
2718
3447
  # where the method call will return nil. Propagate the nil instead of falsely
2719
3448
  # returning false.
2720
- Boolify =
2721
- lambda { |value|
2722
- value == 1 if value
2723
- }
3449
+ Boolify = lambda { |value|
3450
+ case value
3451
+ when 1
3452
+ true
3453
+ when 0
3454
+ false
3455
+ else
3456
+ value
3457
+ end
3458
+ }
2724
3459
 
2725
- BoolifySet =
2726
- lambda { |value|
2727
- if value && "OK" == value
2728
- true
2729
- else
2730
- false
2731
- end
2732
- }
3460
+ BoolifySet = lambda { |value|
3461
+ case value
3462
+ when "OK"
3463
+ true
3464
+ when nil
3465
+ false
3466
+ else
3467
+ value
3468
+ end
3469
+ }
2733
3470
 
2734
- Hashify =
2735
- lambda { |array|
2736
- hash = Hash.new
2737
- array.each_slice(2) do |field, value|
2738
- hash[field] = value
2739
- end
2740
- hash
2741
- }
3471
+ Hashify = lambda { |value|
3472
+ if value.respond_to?(:each_slice)
3473
+ value.each_slice(2).to_h
3474
+ else
3475
+ value
3476
+ end
3477
+ }
3478
+
3479
+ Floatify = lambda { |value|
3480
+ case value
3481
+ when "inf"
3482
+ Float::INFINITY
3483
+ when "-inf"
3484
+ -Float::INFINITY
3485
+ when String
3486
+ Float(value)
3487
+ else
3488
+ value
3489
+ end
3490
+ }
2742
3491
 
2743
- Floatify =
2744
- lambda { |str|
2745
- if str
2746
- if (inf = str.match(/^(-)?inf/i))
2747
- (inf[1] ? -1.0 : 1.0) / 0.0
2748
- else
2749
- Float(str)
2750
- end
2751
- end
3492
+ FloatifyPairs = lambda { |value|
3493
+ return value unless value.respond_to?(:each_slice)
3494
+
3495
+ value.each_slice(2).map do |member, score|
3496
+ [member, Floatify.call(score)]
3497
+ end
3498
+ }
3499
+
3500
+ HashifyInfo = lambda { |reply|
3501
+ lines = reply.split("\r\n").grep_v(/^(#|$)/)
3502
+ lines.map! { |line| line.split(':', 2) }
3503
+ lines.compact!
3504
+ lines.to_h
3505
+ }
3506
+
3507
+ HashifyStreams = lambda { |reply|
3508
+ case reply
3509
+ when nil
3510
+ {}
3511
+ else
3512
+ reply.map { |key, entries| [key, HashifyStreamEntries.call(entries)] }.to_h
3513
+ end
3514
+ }
3515
+
3516
+ EMPTY_STREAM_RESPONSE = [nil].freeze
3517
+ private_constant :EMPTY_STREAM_RESPONSE
3518
+
3519
+ HashifyStreamEntries = lambda { |reply|
3520
+ reply.compact.map do |entry_id, values|
3521
+ [entry_id, values.each_slice(2).to_h]
3522
+ end
3523
+ }
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] }
2752
3529
  }
3530
+ }
2753
3531
 
2754
- FloatifyPairs =
2755
- lambda { |array|
2756
- if array
2757
- array.each_slice(2).map do |member, score|
2758
- [member, Floatify.call(score)]
2759
- end
2760
- end
3532
+ HashifyStreamAutoclaimJustId = lambda { |reply|
3533
+ {
3534
+ 'next' => reply[0],
3535
+ 'entries' => reply[1]
3536
+ }
3537
+ }
3538
+
3539
+ HashifyStreamPendings = lambda { |reply|
3540
+ {
3541
+ 'size' => reply[0],
3542
+ 'min_entry_id' => reply[1],
3543
+ 'max_entry_id' => reply[2],
3544
+ 'consumers' => reply[3].nil? ? {} : reply[3].to_h
2761
3545
  }
3546
+ }
3547
+
3548
+ HashifyStreamPendingDetails = lambda { |reply|
3549
+ reply.map do |arr|
3550
+ {
3551
+ 'entry_id' => arr[0],
3552
+ 'consumer' => arr[1],
3553
+ 'elapsed' => arr[2],
3554
+ 'count' => arr[3]
3555
+ }
3556
+ end
3557
+ }
3558
+
3559
+ HashifyClusterNodeInfo = lambda { |str|
3560
+ arr = str.split(' ')
3561
+ {
3562
+ 'node_id' => arr[0],
3563
+ 'ip_port' => arr[1],
3564
+ 'flags' => arr[2].split(','),
3565
+ 'master_node_id' => arr[3],
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('-'))
3571
+ }
3572
+ }
3573
+
3574
+ HashifyClusterSlots = lambda { |reply|
3575
+ reply.map do |arr|
3576
+ first_slot, last_slot = arr[0..1]
3577
+ master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
3578
+ replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
3579
+ {
3580
+ 'start_slot' => first_slot,
3581
+ 'end_slot' => last_slot,
3582
+ 'master' => master,
3583
+ 'replicas' => replicas
3584
+ }
3585
+ end
3586
+ }
3587
+
3588
+ HashifyClusterNodes = lambda { |reply|
3589
+ reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
3590
+ }
3591
+
3592
+ HashifyClusterSlaves = lambda { |reply|
3593
+ reply.map { |str| HashifyClusterNodeInfo.call(str) }
3594
+ }
3595
+
3596
+ Noop = ->(reply) { reply }
3597
+
3598
+ def _geoarguments(*args, options: nil, sort: nil, count: nil)
3599
+ args.push sort if sort
3600
+ args.push 'count', count if count
3601
+ args.push options if options
3602
+ args
3603
+ end
2762
3604
 
2763
3605
  def _subscription(method, timeout, channels, block)
2764
3606
  return @client.call([method] + channels) if subscribed?
@@ -2775,10 +3617,29 @@ private
2775
3617
  end
2776
3618
  end
2777
3619
 
3620
+ def _xread(args, keys, ids, blocking_timeout_msec)
3621
+ keys = keys.is_a?(Array) ? keys : [keys]
3622
+ ids = ids.is_a?(Array) ? ids : [ids]
3623
+ args << 'STREAMS'
3624
+ args.concat(keys)
3625
+ args.concat(ids)
3626
+
3627
+ synchronize do |client|
3628
+ if blocking_timeout_msec.nil?
3629
+ client.call(args, &HashifyStreams)
3630
+ elsif blocking_timeout_msec.to_f.zero?
3631
+ client.call_without_timeout(args, &HashifyStreams)
3632
+ else
3633
+ timeout = client.timeout.to_f + blocking_timeout_msec.to_f / 1000.0
3634
+ client.call_with_timeout(args, timeout, &HashifyStreams)
3635
+ end
3636
+ end
3637
+ end
2778
3638
  end
2779
3639
 
2780
3640
  require_relative "redis/version"
2781
3641
  require_relative "redis/connection"
2782
3642
  require_relative "redis/client"
3643
+ require_relative "redis/cluster"
2783
3644
  require_relative "redis/pipeline"
2784
3645
  require_relative "redis/subscribe"