redis 3.3.5 → 4.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +132 -2
  3. data/README.md +144 -79
  4. data/lib/redis.rb +1174 -405
  5. data/lib/redis/client.rb +150 -90
  6. data/lib/redis/cluster.rb +295 -0
  7. data/lib/redis/cluster/command.rb +81 -0
  8. data/lib/redis/cluster/command_loader.rb +34 -0
  9. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  10. data/lib/redis/cluster/node.rb +107 -0
  11. data/lib/redis/cluster/node_key.rb +31 -0
  12. data/lib/redis/cluster/node_loader.rb +37 -0
  13. data/lib/redis/cluster/option.rb +93 -0
  14. data/lib/redis/cluster/slot.rb +86 -0
  15. data/lib/redis/cluster/slot_loader.rb +49 -0
  16. data/lib/redis/connection.rb +4 -2
  17. data/lib/redis/connection/command_helper.rb +5 -10
  18. data/lib/redis/connection/hiredis.rb +6 -5
  19. data/lib/redis/connection/registry.rb +2 -1
  20. data/lib/redis/connection/ruby.rb +126 -128
  21. data/lib/redis/connection/synchrony.rb +21 -8
  22. data/lib/redis/distributed.rb +147 -72
  23. data/lib/redis/errors.rb +48 -0
  24. data/lib/redis/hash_ring.rb +30 -73
  25. data/lib/redis/pipeline.rb +55 -15
  26. data/lib/redis/subscribe.rb +11 -12
  27. data/lib/redis/version.rb +3 -1
  28. metadata +49 -202
  29. data/.gitignore +0 -16
  30. data/.travis.yml +0 -89
  31. data/.travis/Gemfile +0 -11
  32. data/.yardopts +0 -3
  33. data/Gemfile +0 -4
  34. data/Rakefile +0 -87
  35. data/benchmarking/logging.rb +0 -71
  36. data/benchmarking/pipeline.rb +0 -51
  37. data/benchmarking/speed.rb +0 -21
  38. data/benchmarking/suite.rb +0 -24
  39. data/benchmarking/worker.rb +0 -71
  40. data/examples/basic.rb +0 -15
  41. data/examples/consistency.rb +0 -114
  42. data/examples/dist_redis.rb +0 -43
  43. data/examples/incr-decr.rb +0 -17
  44. data/examples/list.rb +0 -26
  45. data/examples/pubsub.rb +0 -37
  46. data/examples/sentinel.rb +0 -41
  47. data/examples/sentinel/sentinel.conf +0 -9
  48. data/examples/sentinel/start +0 -49
  49. data/examples/sets.rb +0 -36
  50. data/examples/unicorn/config.ru +0 -3
  51. data/examples/unicorn/unicorn.rb +0 -20
  52. data/redis.gemspec +0 -44
  53. data/test/bitpos_test.rb +0 -69
  54. data/test/blocking_commands_test.rb +0 -42
  55. data/test/client_test.rb +0 -59
  56. data/test/command_map_test.rb +0 -30
  57. data/test/commands_on_hashes_test.rb +0 -21
  58. data/test/commands_on_hyper_log_log_test.rb +0 -21
  59. data/test/commands_on_lists_test.rb +0 -20
  60. data/test/commands_on_sets_test.rb +0 -77
  61. data/test/commands_on_sorted_sets_test.rb +0 -137
  62. data/test/commands_on_strings_test.rb +0 -101
  63. data/test/commands_on_value_types_test.rb +0 -133
  64. data/test/connection_handling_test.rb +0 -277
  65. data/test/connection_test.rb +0 -57
  66. data/test/db/.gitkeep +0 -0
  67. data/test/distributed_blocking_commands_test.rb +0 -46
  68. data/test/distributed_commands_on_hashes_test.rb +0 -10
  69. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  70. data/test/distributed_commands_on_lists_test.rb +0 -22
  71. data/test/distributed_commands_on_sets_test.rb +0 -83
  72. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  73. data/test/distributed_commands_on_strings_test.rb +0 -59
  74. data/test/distributed_commands_on_value_types_test.rb +0 -95
  75. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  76. data/test/distributed_connection_handling_test.rb +0 -23
  77. data/test/distributed_internals_test.rb +0 -79
  78. data/test/distributed_key_tags_test.rb +0 -52
  79. data/test/distributed_persistence_control_commands_test.rb +0 -26
  80. data/test/distributed_publish_subscribe_test.rb +0 -92
  81. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  82. data/test/distributed_scripting_test.rb +0 -102
  83. data/test/distributed_sorting_test.rb +0 -20
  84. data/test/distributed_test.rb +0 -58
  85. data/test/distributed_transactions_test.rb +0 -32
  86. data/test/encoding_test.rb +0 -18
  87. data/test/error_replies_test.rb +0 -59
  88. data/test/fork_safety_test.rb +0 -65
  89. data/test/helper.rb +0 -232
  90. data/test/helper_test.rb +0 -24
  91. data/test/internals_test.rb +0 -417
  92. data/test/lint/blocking_commands.rb +0 -150
  93. data/test/lint/hashes.rb +0 -162
  94. data/test/lint/hyper_log_log.rb +0 -60
  95. data/test/lint/lists.rb +0 -143
  96. data/test/lint/sets.rb +0 -140
  97. data/test/lint/sorted_sets.rb +0 -316
  98. data/test/lint/strings.rb +0 -260
  99. data/test/lint/value_types.rb +0 -122
  100. data/test/persistence_control_commands_test.rb +0 -26
  101. data/test/pipelining_commands_test.rb +0 -242
  102. data/test/publish_subscribe_test.rb +0 -282
  103. data/test/remote_server_control_commands_test.rb +0 -118
  104. data/test/scanning_test.rb +0 -413
  105. data/test/scripting_test.rb +0 -78
  106. data/test/sentinel_command_test.rb +0 -80
  107. data/test/sentinel_test.rb +0 -255
  108. data/test/sorting_test.rb +0 -59
  109. data/test/ssl_test.rb +0 -73
  110. data/test/support/connection/hiredis.rb +0 -1
  111. data/test/support/connection/ruby.rb +0 -1
  112. data/test/support/connection/synchrony.rb +0 -17
  113. data/test/support/redis_mock.rb +0 -130
  114. data/test/support/ssl/gen_certs.sh +0 -31
  115. data/test/support/ssl/trusted-ca.crt +0 -25
  116. data/test/support/ssl/trusted-ca.key +0 -27
  117. data/test/support/ssl/trusted-cert.crt +0 -81
  118. data/test/support/ssl/trusted-cert.key +0 -28
  119. data/test/support/ssl/untrusted-ca.crt +0 -26
  120. data/test/support/ssl/untrusted-ca.key +0 -27
  121. data/test/support/ssl/untrusted-cert.crt +0 -82
  122. data/test/support/ssl/untrusted-cert.key +0 -28
  123. data/test/support/wire/synchrony.rb +0 -24
  124. data/test/support/wire/thread.rb +0 -5
  125. data/test/synchrony_driver.rb +0 -88
  126. data/test/test.conf.erb +0 -9
  127. data/test/thread_safety_test.rb +0 -62
  128. data/test/transactions_test.rb +0 -264
  129. data/test/unknown_commands_test.rb +0 -14
  130. data/test/url_param_test.rb +0 -138
data/lib/redis.rb CHANGED
@@ -1,54 +1,66 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "monitor"
2
- require "redis/errors"
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.deprecate(message, trace = caller[0])
7
- $stderr.puts "\n#{message} (in #{trace})"
8
- end
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."
9
15
 
10
- attr :client
16
+ ::Kernel.warn(message)
17
+ end
18
+
19
+ @exists_returns_integer = value
20
+ end
11
21
 
12
- # @deprecated The preferred way to create a new client object is using `#new`.
13
- # This method does not actually establish a connection to Redis,
14
- # in contrary to what you might expect.
15
- def self.connect(options = {})
16
- new(options)
22
+ attr_writer :current
17
23
  end
18
24
 
19
25
  def self.current
20
26
  @current ||= Redis.new
21
27
  end
22
28
 
23
- def self.current=(redis)
24
- @current = redis
25
- end
26
-
27
29
  include MonitorMixin
28
30
 
29
31
  # Create a new client instance
30
32
  #
31
33
  # @param [Hash] options
32
- # @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.
33
37
  # @option options [String] :host ("127.0.0.1") server hostname
34
- # @option options [Fixnum] :port (6379) server port
38
+ # @option options [Integer] :port (6379) server port
35
39
  # @option options [String] :path path to server socket (overrides host and port)
36
40
  # @option options [Float] :timeout (5.0) timeout in seconds
37
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
38
43
  # @option options [String] :password Password to authenticate against server
39
- # @option options [Fixnum] :db (0) Database to select after initial connect
44
+ # @option options [Integer] :db (0) Database to select after initial connect
40
45
  # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`, `:synchrony`
41
- # @option options [String] :id ID for the client connection, assigns name to current connection by sending `CLIENT SETNAME`
42
- # @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
43
- # @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
44
51
  # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
45
52
  # @option options [Array] :sentinels List of sentinels to contact
46
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
47
57
  #
48
58
  # @return [Redis] a new client instance
49
59
  def initialize(options = {})
50
60
  @options = options.dup
51
- @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)
52
64
  @queue = Hash.new { |h, k| h[k] = [] }
53
65
 
54
66
  super() # Monitor#initialize
@@ -59,7 +71,7 @@ class Redis
59
71
  end
60
72
 
61
73
  # Run code with the client reconnecting
62
- def with_reconnect(val=true, &blk)
74
+ def with_reconnect(val = true, &blk)
63
75
  synchronize do |client|
64
76
  client.with_reconnect(val, &blk)
65
77
  end
@@ -102,7 +114,9 @@ class Redis
102
114
  # See http://redis.io/topics/pipelining for more details.
103
115
  #
104
116
  def queue(*command)
105
- @queue[Thread.current.object_id] << command
117
+ synchronize do
118
+ @queue[Thread.current.object_id] << command
119
+ end
106
120
  end
107
121
 
108
122
  # Sends all commands in the queue.
@@ -112,27 +126,37 @@ class Redis
112
126
  def commit
113
127
  synchronize do |client|
114
128
  begin
115
- 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)
116
135
  ensure
117
136
  @queue.delete(Thread.current.object_id)
118
137
  end
119
138
  end
120
139
  end
121
140
 
141
+ def _client
142
+ @client
143
+ end
144
+
122
145
  # Authenticate to the server.
123
146
  #
124
- # @param [String] password must match the password specified in the
125
- # `requirepass` directive in the configuration file
147
+ # @param [Array<String>] args includes both username and password
148
+ # or only password
126
149
  # @return [String] `OK`
127
- def auth(password)
150
+ # @see https://redis.io/commands/auth AUTH command
151
+ def auth(*args)
128
152
  synchronize do |client|
129
- client.call([:auth, password])
153
+ client.call([:auth, *args])
130
154
  end
131
155
  end
132
156
 
133
157
  # Change the selected database for the current connection.
134
158
  #
135
- # @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)
136
160
  # @return [String] `OK`
137
161
  def select(db)
138
162
  synchronize do |client|
@@ -143,10 +167,11 @@ class Redis
143
167
 
144
168
  # Ping the server.
145
169
  #
170
+ # @param [optional, String] message
146
171
  # @return [String] `PONG`
147
- def ping
172
+ def ping(message = nil)
148
173
  synchronize do |client|
149
- client.call([:ping])
174
+ client.call([:ping, message].compact)
150
175
  end
151
176
  end
152
177
 
@@ -200,7 +225,7 @@ class Redis
200
225
  def config(action, *args)
201
226
  synchronize do |client|
202
227
  client.call([:config, action] + args) do |reply|
203
- if reply.kind_of?(Array) && action == :get
228
+ if reply.is_a?(Array) && action == :get
204
229
  Hashify.call(reply)
205
230
  else
206
231
  reply
@@ -209,9 +234,28 @@ class Redis
209
234
  end
210
235
  end
211
236
 
237
+ # Manage client connections.
238
+ #
239
+ # @param [String, Symbol] subcommand e.g. `kill`, `list`, `getname`, `setname`
240
+ # @return [String, Hash] depends on subcommand
241
+ def client(subcommand = nil, *args)
242
+ synchronize do |client|
243
+ client.call([:client, subcommand] + args) do |reply|
244
+ if subcommand.to_s == "list"
245
+ reply.lines.map do |line|
246
+ entries = line.chomp.split(/[ =]/)
247
+ Hash[entries.each_slice(2).to_a]
248
+ end
249
+ else
250
+ reply
251
+ end
252
+ end
253
+ end
254
+ end
255
+
212
256
  # Return the number of keys in the selected database.
213
257
  #
214
- # @return [Fixnum]
258
+ # @return [Integer]
215
259
  def dbsize
216
260
  synchronize do |client|
217
261
  client.call([:dbsize])
@@ -226,19 +270,31 @@ class Redis
226
270
 
227
271
  # Remove all keys from all databases.
228
272
  #
273
+ # @param [Hash] options
274
+ # - `:async => Boolean`: async flush (default: false)
229
275
  # @return [String] `OK`
230
- def flushall
276
+ def flushall(options = nil)
231
277
  synchronize do |client|
232
- client.call([:flushall])
278
+ if options && options[:async]
279
+ client.call(%i[flushall async])
280
+ else
281
+ client.call([:flushall])
282
+ end
233
283
  end
234
284
  end
235
285
 
236
286
  # Remove all keys from the current database.
237
287
  #
288
+ # @param [Hash] options
289
+ # - `:async => Boolean`: async flush (default: false)
238
290
  # @return [String] `OK`
239
- def flushdb
291
+ def flushdb(options = nil)
240
292
  synchronize do |client|
241
- client.call([:flushdb])
293
+ if options && options[:async]
294
+ client.call(%i[flushdb async])
295
+ else
296
+ client.call([:flushdb])
297
+ end
242
298
  end
243
299
  end
244
300
 
@@ -249,10 +305,8 @@ class Redis
249
305
  def info(cmd = nil)
250
306
  synchronize do |client|
251
307
  client.call([:info, cmd].compact) do |reply|
252
- if reply.kind_of?(String)
253
- reply = Hash[reply.split("\r\n").map do |line|
254
- line.split(":", 2) unless line =~ /^(#|$)/
255
- end.compact]
308
+ if reply.is_a?(String)
309
+ reply = HashifyInfo.call(reply)
256
310
 
257
311
  if cmd && cmd.to_s == "commandstats"
258
312
  # Extract nested hashes for INFO COMMANDSTATS
@@ -270,7 +324,7 @@ class Redis
270
324
 
271
325
  # Get the UNIX time stamp of the last successful save to disk.
272
326
  #
273
- # @return [Fixnum]
327
+ # @return [Integer]
274
328
  def lastsave
275
329
  synchronize do |client|
276
330
  client.call([:lastsave])
@@ -322,9 +376,9 @@ class Redis
322
376
  # Interact with the slowlog (get, len, reset)
323
377
  #
324
378
  # @param [String] subcommand e.g. `get`, `len`, `reset`
325
- # @param [Fixnum] length maximum number of entries to return
326
- # @return [Array<String>, Fixnum, String] depends on subcommand
327
- 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)
328
382
  synchronize do |client|
329
383
  args = [:slowlog, subcommand]
330
384
  args << length if length
@@ -344,12 +398,12 @@ class Redis
344
398
  # @example
345
399
  # r.time # => [ 1333093196, 606806 ]
346
400
  #
347
- # @return [Array<Fixnum>] tuple of seconds since UNIX epoch and
401
+ # @return [Array<Integer>] tuple of seconds since UNIX epoch and
348
402
  # microseconds in the current second
349
403
  def time
350
404
  synchronize do |client|
351
405
  client.call([:time]) do |reply|
352
- reply.map(&:to_i) if reply
406
+ reply&.map(&:to_i)
353
407
  end
354
408
  end
355
409
  end
@@ -367,7 +421,7 @@ class Redis
367
421
  # Set a key's time to live in seconds.
368
422
  #
369
423
  # @param [String] key
370
- # @param [Fixnum] seconds time to live
424
+ # @param [Integer] seconds time to live
371
425
  # @return [Boolean] whether the timeout was set or not
372
426
  def expire(key, seconds)
373
427
  synchronize do |client|
@@ -378,7 +432,7 @@ class Redis
378
432
  # Set the expiration for a key as a UNIX timestamp.
379
433
  #
380
434
  # @param [String] key
381
- # @param [Fixnum] unix_time expiry time specified as a UNIX timestamp
435
+ # @param [Integer] unix_time expiry time specified as a UNIX timestamp
382
436
  # @return [Boolean] whether the timeout was set or not
383
437
  def expireat(key, unix_time)
384
438
  synchronize do |client|
@@ -389,7 +443,7 @@ class Redis
389
443
  # Get the time to live (in seconds) for a key.
390
444
  #
391
445
  # @param [String] key
392
- # @return [Fixnum] remaining time to live in seconds.
446
+ # @return [Integer] remaining time to live in seconds.
393
447
  #
394
448
  # In Redis 2.6 or older the command returns -1 if the key does not exist or if
395
449
  # the key exist but has no associated expire.
@@ -407,7 +461,7 @@ class Redis
407
461
  # Set a key's time to live in milliseconds.
408
462
  #
409
463
  # @param [String] key
410
- # @param [Fixnum] milliseconds time to live
464
+ # @param [Integer] milliseconds time to live
411
465
  # @return [Boolean] whether the timeout was set or not
412
466
  def pexpire(key, milliseconds)
413
467
  synchronize do |client|
@@ -418,7 +472,7 @@ class Redis
418
472
  # Set the expiration for a key as number of milliseconds from UNIX Epoch.
419
473
  #
420
474
  # @param [String] key
421
- # @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.
422
476
  # @return [Boolean] whether the timeout was set or not
423
477
  def pexpireat(key, ms_unix_time)
424
478
  synchronize do |client|
@@ -429,7 +483,7 @@ class Redis
429
483
  # Get the time to live (in milliseconds) for a key.
430
484
  #
431
485
  # @param [String] key
432
- # @return [Fixnum] remaining time to live in milliseconds
486
+ # @return [Integer] remaining time to live in milliseconds
433
487
  # In Redis 2.6 or older the command returns -1 if the key does not exist or if
434
488
  # the key exist but has no associated expire.
435
489
  #
@@ -458,50 +512,101 @@ class Redis
458
512
  # @param [String] key
459
513
  # @param [String] ttl
460
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]
461
518
  # @return [String] `"OK"`
462
- 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
+
463
523
  synchronize do |client|
464
- client.call([:restore, key, ttl, serialized_value])
524
+ client.call(args)
465
525
  end
466
526
  end
467
527
 
468
528
  # Transfer a key from the connected instance to another instance.
469
529
  #
470
- # @param [String] key
530
+ # @param [String, Array<String>] key
471
531
  # @param [Hash] options
472
532
  # - `:host => String`: host of instance to migrate to
473
533
  # - `:port => Integer`: port of instance to migrate to
474
534
  # - `:db => Integer`: database to migrate to (default: same as source)
475
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.
476
538
  # @return [String] `"OK"`
477
539
  def migrate(key, options)
478
- host = options[:host] || raise(RuntimeError, ":host not specified")
479
- port = options[:port] || raise(RuntimeError, ":port not specified")
480
- db = (options[:db] || client.db).to_i
481
- 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)
482
549
 
483
- synchronize do |client|
484
- client.call([:migrate, host, port, key, db, timeout])
485
- end
550
+ synchronize { |client| client.call(args) }
486
551
  end
487
552
 
488
553
  # Delete one or more keys.
489
554
  #
490
555
  # @param [String, Array<String>] keys
491
- # @return [Fixnum] number of keys that were deleted
556
+ # @return [Integer] number of keys that were deleted
492
557
  def del(*keys)
493
558
  synchronize do |client|
494
559
  client.call([:del] + keys)
495
560
  end
496
561
  end
497
562
 
498
- # Determine if a key exists.
563
+ # Unlink one or more keys.
499
564
  #
500
- # @param [String] key
565
+ # @param [String, Array<String>] keys
566
+ # @return [Integer] number of keys that were unlinked
567
+ def unlink(*keys)
568
+ synchronize do |client|
569
+ client.call([:unlink] + keys)
570
+ end
571
+ end
572
+
573
+ # Determine how many of the keys exists.
574
+ #
575
+ # @param [String, Array<String>] keys
576
+ # @return [Integer]
577
+ def exists(*keys)
578
+ if !Redis.exists_returns_integer && keys.size == 1
579
+ if Redis.exists_returns_integer.nil?
580
+ message = "`Redis#exists(key)` will return an Integer in redis-rb 4.3. `exists?` returns a boolean, you " \
581
+ "should use it instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = " \
582
+ "true. To disable this message and keep the current (boolean) behaviour of 'exists' you can set " \
583
+ "`Redis.exists_returns_integer = false`, but this option will be removed in 5.0. " \
584
+ "(#{::Kernel.caller(1, 1).first})\n"
585
+
586
+ ::Kernel.warn(message)
587
+ end
588
+
589
+ exists?(*keys)
590
+ else
591
+ _exists(*keys)
592
+ end
593
+ end
594
+
595
+ def _exists(*keys)
596
+ synchronize do |client|
597
+ client.call([:exists, *keys])
598
+ end
599
+ end
600
+
601
+ # Determine if any of the keys exists.
602
+ #
603
+ # @param [String, Array<String>] keys
501
604
  # @return [Boolean]
502
- def exists(key)
605
+ def exists?(*keys)
503
606
  synchronize do |client|
504
- client.call([:exists, key], &Boolify)
607
+ client.call([:exists, *keys]) do |value|
608
+ value > 0
609
+ end
505
610
  end
506
611
  end
507
612
 
@@ -512,7 +617,7 @@ class Redis
512
617
  def keys(pattern = "*")
513
618
  synchronize do |client|
514
619
  client.call([:keys, pattern]) do |reply|
515
- if reply.kind_of?(String)
620
+ if reply.is_a?(String)
516
621
  reply.split(" ")
517
622
  else
518
623
  reply
@@ -538,7 +643,7 @@ class Redis
538
643
  # # => "bar"
539
644
  #
540
645
  # @param [String] key
541
- # @param [Fixnum] db
646
+ # @param [Integer] db
542
647
  # @return [Boolean] whether the key was moved or not
543
648
  def move(key, db)
544
649
  synchronize do |client|
@@ -602,36 +707,33 @@ class Redis
602
707
  # - `:order => String`: combination of `ASC`, `DESC` and optionally `ALPHA`
603
708
  # - `:store => String`: key to store the result at
604
709
  #
605
- # @return [Array<String>, Array<Array<String>>, Fixnum]
710
+ # @return [Array<String>, Array<Array<String>>, Integer]
606
711
  # - when `:get` is not specified, or holds a single element, an array of elements
607
712
  # - when `:get` is specified, and holds more than one element, an array of
608
713
  # elements where every element is an array with the result for every
609
714
  # element specified in `:get`
610
715
  # - when `:store` is specified, the number of elements in the stored result
611
- def sort(key, options = {})
612
- args = []
613
-
614
- by = options[:by]
615
- args.concat(["BY", by]) if by
716
+ def sort(key, by: nil, limit: nil, get: nil, order: nil, store: nil)
717
+ args = [:sort, key]
718
+ args << "BY" << by if by
616
719
 
617
- limit = options[:limit]
618
- args.concat(["LIMIT"] + limit) if limit
720
+ if limit
721
+ args << "LIMIT"
722
+ args.concat(limit)
723
+ end
619
724
 
620
- get = Array(options[:get])
621
- args.concat(["GET"].product(get).flatten) unless get.empty?
725
+ get = Array(get)
726
+ get.each do |item|
727
+ args << "GET" << item
728
+ end
622
729
 
623
- order = options[:order]
624
730
  args.concat(order.split(" ")) if order
625
-
626
- store = options[:store]
627
- args.concat(["STORE", store]) if store
731
+ args << "STORE" << store if store
628
732
 
629
733
  synchronize do |client|
630
- client.call([:sort, key] + args) do |reply|
734
+ client.call(args) do |reply|
631
735
  if get.size > 1 && !store
632
- if reply
633
- reply.each_slice(get.size).to_a
634
- end
736
+ reply.each_slice(get.size).to_a if reply
635
737
  else
636
738
  reply
637
739
  end
@@ -656,7 +758,7 @@ class Redis
656
758
  # # => 4
657
759
  #
658
760
  # @param [String] key
659
- # @return [Fixnum] value after decrementing it
761
+ # @return [Integer] value after decrementing it
660
762
  def decr(key)
661
763
  synchronize do |client|
662
764
  client.call([:decr, key])
@@ -670,8 +772,8 @@ class Redis
670
772
  # # => 0
671
773
  #
672
774
  # @param [String] key
673
- # @param [Fixnum] decrement
674
- # @return [Fixnum] value after decrementing it
775
+ # @param [Integer] decrement
776
+ # @return [Integer] value after decrementing it
675
777
  def decrby(key, decrement)
676
778
  synchronize do |client|
677
779
  client.call([:decrby, key, decrement])
@@ -685,7 +787,7 @@ class Redis
685
787
  # # => 6
686
788
  #
687
789
  # @param [String] key
688
- # @return [Fixnum] value after incrementing it
790
+ # @return [Integer] value after incrementing it
689
791
  def incr(key)
690
792
  synchronize do |client|
691
793
  client.call([:incr, key])
@@ -699,8 +801,8 @@ class Redis
699
801
  # # => 10
700
802
  #
701
803
  # @param [String] key
702
- # @param [Fixnum] increment
703
- # @return [Fixnum] value after incrementing it
804
+ # @param [Integer] increment
805
+ # @return [Integer] value after incrementing it
704
806
  def incrby(key, increment)
705
807
  synchronize do |client|
706
808
  client.call([:incrby, key, increment])
@@ -727,41 +829,33 @@ class Redis
727
829
  # @param [String] key
728
830
  # @param [String] value
729
831
  # @param [Hash] options
730
- # - `:ex => Fixnum`: Set the specified expire time, in seconds.
731
- # - `:px => Fixnum`: Set the specified expire time, in milliseconds.
832
+ # - `:ex => Integer`: Set the specified expire time, in seconds.
833
+ # - `:px => Integer`: Set the specified expire time, in milliseconds.
732
834
  # - `:nx => true`: Only set the key if it does not already exist.
733
835
  # - `:xx => true`: Only set the key if it already exist.
836
+ # - `:keepttl => true`: Retain the time to live associated with the key.
734
837
  # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
735
- def set(key, value, options = {})
736
- args = []
737
-
738
- ex = options[:ex]
739
- args.concat(["EX", ex]) if ex
740
-
741
- px = options[:px]
742
- args.concat(["PX", px]) if px
743
-
744
- nx = options[:nx]
745
- args.concat(["NX"]) if nx
746
-
747
- xx = options[:xx]
748
- args.concat(["XX"]) if xx
838
+ def set(key, value, ex: nil, px: nil, nx: nil, xx: nil, keepttl: nil)
839
+ args = [:set, key, value.to_s]
840
+ args << "EX" << ex if ex
841
+ args << "PX" << px if px
842
+ args << "NX" if nx
843
+ args << "XX" if xx
844
+ args << "KEEPTTL" if keepttl
749
845
 
750
846
  synchronize do |client|
751
847
  if nx || xx
752
- client.call([:set, key, value.to_s] + args, &BoolifySet)
848
+ client.call(args, &BoolifySet)
753
849
  else
754
- client.call([:set, key, value.to_s] + args)
850
+ client.call(args)
755
851
  end
756
852
  end
757
853
  end
758
854
 
759
- alias :[]= :set
760
-
761
855
  # Set the time to live in seconds of a key.
762
856
  #
763
857
  # @param [String] key
764
- # @param [Fixnum] ttl
858
+ # @param [Integer] ttl
765
859
  # @param [String] value
766
860
  # @return [String] `"OK"`
767
861
  def setex(key, ttl, value)
@@ -773,7 +867,7 @@ class Redis
773
867
  # Set the time to live in milliseconds of a key.
774
868
  #
775
869
  # @param [String] key
776
- # @param [Fixnum] ttl
870
+ # @param [Integer] ttl
777
871
  # @param [String] value
778
872
  # @return [String] `"OK"`
779
873
  def psetex(key, ttl, value)
@@ -835,7 +929,7 @@ class Redis
835
929
  # @see #mapped_msetnx
836
930
  def msetnx(*args)
837
931
  synchronize do |client|
838
- client.call([:msetnx] + args, &Boolify)
932
+ client.call([:msetnx, *args], &Boolify)
839
933
  end
840
934
  end
841
935
 
@@ -863,12 +957,10 @@ class Redis
863
957
  end
864
958
  end
865
959
 
866
- alias :[] :get
867
-
868
960
  # Get the values of all the given keys.
869
961
  #
870
962
  # @example
871
- # redis.mget("key1", "key1")
963
+ # redis.mget("key1", "key2")
872
964
  # # => ["v1", "v2"]
873
965
  #
874
966
  # @param [Array<String>] keys
@@ -877,7 +969,7 @@ class Redis
877
969
  # @see #mapped_mget
878
970
  def mget(*keys, &blk)
879
971
  synchronize do |client|
880
- client.call([:mget] + keys, &blk)
972
+ client.call([:mget, *keys], &blk)
881
973
  end
882
974
  end
883
975
 
@@ -893,7 +985,7 @@ class Redis
893
985
  # @see #mget
894
986
  def mapped_mget(*keys)
895
987
  mget(*keys) do |reply|
896
- if reply.kind_of?(Array)
988
+ if reply.is_a?(Array)
897
989
  Hash[keys.zip(reply)]
898
990
  else
899
991
  reply
@@ -904,9 +996,9 @@ class Redis
904
996
  # Overwrite part of a string at key starting at the specified offset.
905
997
  #
906
998
  # @param [String] key
907
- # @param [Fixnum] offset byte offset
999
+ # @param [Integer] offset byte offset
908
1000
  # @param [String] value
909
- # @return [Fixnum] length of the string after it was modified
1001
+ # @return [Integer] length of the string after it was modified
910
1002
  def setrange(key, offset, value)
911
1003
  synchronize do |client|
912
1004
  client.call([:setrange, key, offset, value.to_s])
@@ -916,10 +1008,10 @@ class Redis
916
1008
  # Get a substring of the string stored at a key.
917
1009
  #
918
1010
  # @param [String] key
919
- # @param [Fixnum] start zero-based start offset
920
- # @param [Fixnum] stop zero-based end offset. Use -1 for representing
1011
+ # @param [Integer] start zero-based start offset
1012
+ # @param [Integer] stop zero-based end offset. Use -1 for representing
921
1013
  # the end of the string
922
- # @return [Fixnum] `0` or `1`
1014
+ # @return [Integer] `0` or `1`
923
1015
  def getrange(key, start, stop)
924
1016
  synchronize do |client|
925
1017
  client.call([:getrange, key, start, stop])
@@ -929,9 +1021,9 @@ class Redis
929
1021
  # Sets or clears the bit at offset in the string value stored at key.
930
1022
  #
931
1023
  # @param [String] key
932
- # @param [Fixnum] offset bit offset
933
- # @param [Fixnum] value bit value `0` or `1`
934
- # @return [Fixnum] the original bit value stored at `offset`
1024
+ # @param [Integer] offset bit offset
1025
+ # @param [Integer] value bit value `0` or `1`
1026
+ # @return [Integer] the original bit value stored at `offset`
935
1027
  def setbit(key, offset, value)
936
1028
  synchronize do |client|
937
1029
  client.call([:setbit, key, offset, value])
@@ -941,8 +1033,8 @@ class Redis
941
1033
  # Returns the bit value at offset in the string value stored at key.
942
1034
  #
943
1035
  # @param [String] key
944
- # @param [Fixnum] offset bit offset
945
- # @return [Fixnum] `0` or `1`
1036
+ # @param [Integer] offset bit offset
1037
+ # @return [Integer] `0` or `1`
946
1038
  def getbit(key, offset)
947
1039
  synchronize do |client|
948
1040
  client.call([:getbit, key, offset])
@@ -953,7 +1045,7 @@ class Redis
953
1045
  #
954
1046
  # @param [String] key
955
1047
  # @param [String] value value to append
956
- # @return [Fixnum] length of the string after appending
1048
+ # @return [Integer] length of the string after appending
957
1049
  def append(key, value)
958
1050
  synchronize do |client|
959
1051
  client.call([:append, key, value])
@@ -963,9 +1055,9 @@ class Redis
963
1055
  # Count the number of set bits in a range of the string value stored at key.
964
1056
  #
965
1057
  # @param [String] key
966
- # @param [Fixnum] start start index
967
- # @param [Fixnum] stop stop index
968
- # @return [Fixnum] the number of bits set to 1
1058
+ # @param [Integer] start start index
1059
+ # @param [Integer] stop stop index
1060
+ # @return [Integer] the number of bits set to 1
969
1061
  def bitcount(key, start = 0, stop = -1)
970
1062
  synchronize do |client|
971
1063
  client.call([:bitcount, key, start, stop])
@@ -977,25 +1069,23 @@ class Redis
977
1069
  # @param [String] operation e.g. `and`, `or`, `xor`, `not`
978
1070
  # @param [String] destkey destination key
979
1071
  # @param [String, Array<String>] keys one or more source keys to perform `operation`
980
- # @return [Fixnum] the length of the string stored in `destkey`
1072
+ # @return [Integer] the length of the string stored in `destkey`
981
1073
  def bitop(operation, destkey, *keys)
982
1074
  synchronize do |client|
983
- client.call([:bitop, operation, destkey] + keys)
1075
+ client.call([:bitop, operation, destkey, *keys])
984
1076
  end
985
1077
  end
986
1078
 
987
1079
  # Return the position of the first bit set to 1 or 0 in a string.
988
1080
  #
989
1081
  # @param [String] key
990
- # @param [Fixnum] bit whether to look for the first 1 or 0 bit
991
- # @param [Fixnum] start start index
992
- # @param [Fixnum] stop stop index
993
- # @return [Fixnum] the position of the first 1/0 bit.
1082
+ # @param [Integer] bit whether to look for the first 1 or 0 bit
1083
+ # @param [Integer] start start index
1084
+ # @param [Integer] stop stop index
1085
+ # @return [Integer] the position of the first 1/0 bit.
994
1086
  # -1 if looking for 1 and it is not found or start and stop are given.
995
- def bitpos(key, bit, start=nil, stop=nil)
996
- if stop and not start
997
- raise(ArgumentError, 'stop parameter specified without start parameter')
998
- end
1087
+ def bitpos(key, bit, start = nil, stop = nil)
1088
+ raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
999
1089
 
1000
1090
  synchronize do |client|
1001
1091
  command = [:bitpos, key, bit]
@@ -1020,7 +1110,7 @@ class Redis
1020
1110
  # Get the length of the value stored in a key.
1021
1111
  #
1022
1112
  # @param [String] key
1023
- # @return [Fixnum] the length of the value stored in the key, or 0
1113
+ # @return [Integer] the length of the value stored in the key, or 0
1024
1114
  # if the key does not exist
1025
1115
  def strlen(key)
1026
1116
  synchronize do |client|
@@ -1031,7 +1121,7 @@ class Redis
1031
1121
  # Get the length of a list.
1032
1122
  #
1033
1123
  # @param [String] key
1034
- # @return [Fixnum]
1124
+ # @return [Integer]
1035
1125
  def llen(key)
1036
1126
  synchronize do |client|
1037
1127
  client.call([:llen, key])
@@ -1041,8 +1131,8 @@ class Redis
1041
1131
  # Prepend one or more values to a list, creating the list if it doesn't exist
1042
1132
  #
1043
1133
  # @param [String] key
1044
- # @param [String, Array] value string value, or array of string values to push
1045
- # @return [Fixnum] the length of the list after the push operation
1134
+ # @param [String, Array<String>] value string value, or array of string values to push
1135
+ # @return [Integer] the length of the list after the push operation
1046
1136
  def lpush(key, value)
1047
1137
  synchronize do |client|
1048
1138
  client.call([:lpush, key, value])
@@ -1053,7 +1143,7 @@ class Redis
1053
1143
  #
1054
1144
  # @param [String] key
1055
1145
  # @param [String] value
1056
- # @return [Fixnum] the length of the list after the push operation
1146
+ # @return [Integer] the length of the list after the push operation
1057
1147
  def lpushx(key, value)
1058
1148
  synchronize do |client|
1059
1149
  client.call([:lpushx, key, value])
@@ -1063,8 +1153,8 @@ class Redis
1063
1153
  # Append one or more values to a list, creating the list if it doesn't exist
1064
1154
  #
1065
1155
  # @param [String] key
1066
- # @param [String] value
1067
- # @return [Fixnum] the length of the list after the push operation
1156
+ # @param [String, Array<String>] value string value, or array of string values to push
1157
+ # @return [Integer] the length of the list after the push operation
1068
1158
  def rpush(key, value)
1069
1159
  synchronize do |client|
1070
1160
  client.call([:rpush, key, value])
@@ -1075,30 +1165,36 @@ class Redis
1075
1165
  #
1076
1166
  # @param [String] key
1077
1167
  # @param [String] value
1078
- # @return [Fixnum] the length of the list after the push operation
1168
+ # @return [Integer] the length of the list after the push operation
1079
1169
  def rpushx(key, value)
1080
1170
  synchronize do |client|
1081
1171
  client.call([:rpushx, key, value])
1082
1172
  end
1083
1173
  end
1084
1174
 
1085
- # Remove and get the first element in a list.
1175
+ # Remove and get the first elements in a list.
1086
1176
  #
1087
1177
  # @param [String] key
1088
- # @return [String]
1089
- def lpop(key)
1178
+ # @param [Integer] count number of elements to remove
1179
+ # @return [String, Array<String>] the values of the first elements
1180
+ def lpop(key, count = nil)
1090
1181
  synchronize do |client|
1091
- client.call([:lpop, key])
1182
+ command = [:lpop, key]
1183
+ command << count if count
1184
+ client.call(command)
1092
1185
  end
1093
1186
  end
1094
1187
 
1095
- # Remove and get the last element in a list.
1188
+ # Remove and get the last elements in a list.
1096
1189
  #
1097
1190
  # @param [String] key
1098
- # @return [String]
1099
- def rpop(key)
1191
+ # @param [Integer] count number of elements to remove
1192
+ # @return [String, Array<String>] the values of the last elements
1193
+ def rpop(key, count = nil)
1100
1194
  synchronize do |client|
1101
- client.call([:rpop, key])
1195
+ command = [:rpop, key]
1196
+ command << count if count
1197
+ client.call(command)
1102
1198
  end
1103
1199
  end
1104
1200
 
@@ -1113,28 +1209,27 @@ class Redis
1113
1209
  end
1114
1210
  end
1115
1211
 
1116
- def _bpop(cmd, args)
1117
- options = {}
1118
-
1119
- case args.last
1120
- when Hash
1212
+ def _bpop(cmd, args, &blk)
1213
+ timeout = if args.last.is_a?(Hash)
1121
1214
  options = args.pop
1122
- when Integer
1215
+ options[:timeout]
1216
+ elsif args.last.respond_to?(:to_int)
1123
1217
  # Issue deprecation notice in obnoxious mode...
1124
- options[:timeout] = args.pop
1218
+ args.pop.to_int
1125
1219
  end
1126
1220
 
1221
+ timeout ||= 0
1222
+
1127
1223
  if args.size > 1
1128
1224
  # Issue deprecation notice in obnoxious mode...
1129
1225
  end
1130
1226
 
1131
1227
  keys = args.flatten
1132
- timeout = options[:timeout] || 0
1133
1228
 
1134
1229
  synchronize do |client|
1135
1230
  command = [cmd, keys, timeout]
1136
1231
  timeout += client.timeout if timeout > 0
1137
- client.call_with_timeout(command, timeout)
1232
+ client.call_with_timeout(command, timeout, &blk)
1138
1233
  end
1139
1234
  end
1140
1235
 
@@ -1154,7 +1249,7 @@ class Redis
1154
1249
  # @param [String, Array<String>] keys one or more keys to perform the
1155
1250
  # blocking pop on
1156
1251
  # @param [Hash] options
1157
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1252
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
1158
1253
  #
1159
1254
  # @return [nil, [String, String]]
1160
1255
  # - `nil` when the operation timed out
@@ -1168,7 +1263,7 @@ class Redis
1168
1263
  # @param [String, Array<String>] keys one or more keys to perform the
1169
1264
  # blocking pop on
1170
1265
  # @param [Hash] options
1171
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1266
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
1172
1267
  #
1173
1268
  # @return [nil, [String, String]]
1174
1269
  # - `nil` when the operation timed out
@@ -1185,20 +1280,12 @@ class Redis
1185
1280
  # @param [String] source source key
1186
1281
  # @param [String] destination destination key
1187
1282
  # @param [Hash] options
1188
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1283
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
1189
1284
  #
1190
1285
  # @return [nil, String]
1191
1286
  # - `nil` when the operation timed out
1192
1287
  # - the element was popped and pushed otherwise
1193
- def brpoplpush(source, destination, options = {})
1194
- case options
1195
- when Integer
1196
- # Issue deprecation notice in obnoxious mode...
1197
- options = { :timeout => options }
1198
- end
1199
-
1200
- timeout = options[:timeout] || 0
1201
-
1288
+ def brpoplpush(source, destination, deprecated_timeout = 0, timeout: deprecated_timeout)
1202
1289
  synchronize do |client|
1203
1290
  command = [:brpoplpush, source, destination, timeout]
1204
1291
  timeout += client.timeout if timeout > 0
@@ -1209,7 +1296,7 @@ class Redis
1209
1296
  # Get an element from a list by its index.
1210
1297
  #
1211
1298
  # @param [String] key
1212
- # @param [Fixnum] index
1299
+ # @param [Integer] index
1213
1300
  # @return [String]
1214
1301
  def lindex(key, index)
1215
1302
  synchronize do |client|
@@ -1223,7 +1310,7 @@ class Redis
1223
1310
  # @param [String, Symbol] where `BEFORE` or `AFTER`
1224
1311
  # @param [String] pivot reference element
1225
1312
  # @param [String] value
1226
- # @return [Fixnum] length of the list after the insert operation, or `-1`
1313
+ # @return [Integer] length of the list after the insert operation, or `-1`
1227
1314
  # when the element `pivot` was not found
1228
1315
  def linsert(key, where, pivot, value)
1229
1316
  synchronize do |client|
@@ -1234,8 +1321,8 @@ class Redis
1234
1321
  # Get a range of elements from a list.
1235
1322
  #
1236
1323
  # @param [String] key
1237
- # @param [Fixnum] start start index
1238
- # @param [Fixnum] stop stop index
1324
+ # @param [Integer] start start index
1325
+ # @param [Integer] stop stop index
1239
1326
  # @return [Array<String>]
1240
1327
  def lrange(key, start, stop)
1241
1328
  synchronize do |client|
@@ -1246,12 +1333,12 @@ class Redis
1246
1333
  # Remove elements from a list.
1247
1334
  #
1248
1335
  # @param [String] key
1249
- # @param [Fixnum] count number of elements to remove. Use a positive
1336
+ # @param [Integer] count number of elements to remove. Use a positive
1250
1337
  # value to remove the first `count` occurrences of `value`. A negative
1251
1338
  # value to remove the last `count` occurrences of `value`. Or zero, to
1252
1339
  # remove all occurrences of `value` from the list.
1253
1340
  # @param [String] value
1254
- # @return [Fixnum] the number of removed elements
1341
+ # @return [Integer] the number of removed elements
1255
1342
  def lrem(key, count, value)
1256
1343
  synchronize do |client|
1257
1344
  client.call([:lrem, key, count, value])
@@ -1261,7 +1348,7 @@ class Redis
1261
1348
  # Set the value of an element in a list by its index.
1262
1349
  #
1263
1350
  # @param [String] key
1264
- # @param [Fixnum] index
1351
+ # @param [Integer] index
1265
1352
  # @param [String] value
1266
1353
  # @return [String] `OK`
1267
1354
  def lset(key, index, value)
@@ -1273,8 +1360,8 @@ class Redis
1273
1360
  # Trim a list to the specified range.
1274
1361
  #
1275
1362
  # @param [String] key
1276
- # @param [Fixnum] start start index
1277
- # @param [Fixnum] stop stop index
1363
+ # @param [Integer] start start index
1364
+ # @param [Integer] stop stop index
1278
1365
  # @return [String] `OK`
1279
1366
  def ltrim(key, start, stop)
1280
1367
  synchronize do |client|
@@ -1285,7 +1372,7 @@ class Redis
1285
1372
  # Get the number of members in a set.
1286
1373
  #
1287
1374
  # @param [String] key
1288
- # @return [Fixnum]
1375
+ # @return [Integer]
1289
1376
  def scard(key)
1290
1377
  synchronize do |client|
1291
1378
  client.call([:scard, key])
@@ -1296,8 +1383,8 @@ class Redis
1296
1383
  #
1297
1384
  # @param [String] key
1298
1385
  # @param [String, Array<String>] member one member, or array of members
1299
- # @return [Boolean, Fixnum] `Boolean` when a single member is specified,
1300
- # holding whether or not adding the member succeeded, or `Fixnum` when an
1386
+ # @return [Boolean, Integer] `Boolean` when a single member is specified,
1387
+ # holding whether or not adding the member succeeded, or `Integer` when an
1301
1388
  # array of members is specified, holding the number of members that were
1302
1389
  # successfully added
1303
1390
  def sadd(key, member)
@@ -1318,8 +1405,8 @@ class Redis
1318
1405
  #
1319
1406
  # @param [String] key
1320
1407
  # @param [String, Array<String>] member one member, or array of members
1321
- # @return [Boolean, Fixnum] `Boolean` when a single member is specified,
1322
- # holding whether or not removing the member succeeded, or `Fixnum` when an
1408
+ # @return [Boolean, Integer] `Boolean` when a single member is specified,
1409
+ # holding whether or not removing the member succeeded, or `Integer` when an
1323
1410
  # array of members is specified, holding the number of members that were
1324
1411
  # successfully removed
1325
1412
  def srem(key, member)
@@ -1340,7 +1427,7 @@ class Redis
1340
1427
  #
1341
1428
  # @param [String] key
1342
1429
  # @return [String]
1343
- # @param [Fixnum] count
1430
+ # @param [Integer] count
1344
1431
  def spop(key, count = nil)
1345
1432
  synchronize do |client|
1346
1433
  if count.nil?
@@ -1354,7 +1441,7 @@ class Redis
1354
1441
  # Get one or more random members from a set.
1355
1442
  #
1356
1443
  # @param [String] key
1357
- # @param [Fixnum] count
1444
+ # @param [Integer] count
1358
1445
  # @return [String]
1359
1446
  def srandmember(key, count = nil)
1360
1447
  synchronize do |client|
@@ -1405,7 +1492,7 @@ class Redis
1405
1492
  # @return [Array<String>] members in the difference
1406
1493
  def sdiff(*keys)
1407
1494
  synchronize do |client|
1408
- client.call([:sdiff] + keys)
1495
+ client.call([:sdiff, *keys])
1409
1496
  end
1410
1497
  end
1411
1498
 
@@ -1413,10 +1500,10 @@ class Redis
1413
1500
  #
1414
1501
  # @param [String] destination destination key
1415
1502
  # @param [String, Array<String>] keys keys pointing to sets to subtract
1416
- # @return [Fixnum] number of elements in the resulting set
1503
+ # @return [Integer] number of elements in the resulting set
1417
1504
  def sdiffstore(destination, *keys)
1418
1505
  synchronize do |client|
1419
- client.call([:sdiffstore, destination] + keys)
1506
+ client.call([:sdiffstore, destination, *keys])
1420
1507
  end
1421
1508
  end
1422
1509
 
@@ -1426,7 +1513,7 @@ class Redis
1426
1513
  # @return [Array<String>] members in the intersection
1427
1514
  def sinter(*keys)
1428
1515
  synchronize do |client|
1429
- client.call([:sinter] + keys)
1516
+ client.call([:sinter, *keys])
1430
1517
  end
1431
1518
  end
1432
1519
 
@@ -1434,10 +1521,10 @@ class Redis
1434
1521
  #
1435
1522
  # @param [String] destination destination key
1436
1523
  # @param [String, Array<String>] keys keys pointing to sets to intersect
1437
- # @return [Fixnum] number of elements in the resulting set
1524
+ # @return [Integer] number of elements in the resulting set
1438
1525
  def sinterstore(destination, *keys)
1439
1526
  synchronize do |client|
1440
- client.call([:sinterstore, destination] + keys)
1527
+ client.call([:sinterstore, destination, *keys])
1441
1528
  end
1442
1529
  end
1443
1530
 
@@ -1447,7 +1534,7 @@ class Redis
1447
1534
  # @return [Array<String>] members in the union
1448
1535
  def sunion(*keys)
1449
1536
  synchronize do |client|
1450
- client.call([:sunion] + keys)
1537
+ client.call([:sunion, *keys])
1451
1538
  end
1452
1539
  end
1453
1540
 
@@ -1455,10 +1542,10 @@ class Redis
1455
1542
  #
1456
1543
  # @param [String] destination destination key
1457
1544
  # @param [String, Array<String>] keys keys pointing to sets to unify
1458
- # @return [Fixnum] number of elements in the resulting set
1545
+ # @return [Integer] number of elements in the resulting set
1459
1546
  def sunionstore(destination, *keys)
1460
1547
  synchronize do |client|
1461
- client.call([:sunionstore, destination] + keys)
1548
+ client.call([:sunionstore, destination, *keys])
1462
1549
  end
1463
1550
  end
1464
1551
 
@@ -1469,7 +1556,7 @@ class Redis
1469
1556
  # # => 4
1470
1557
  #
1471
1558
  # @param [String] key
1472
- # @return [Fixnum]
1559
+ # @return [Integer]
1473
1560
  def zcard(key)
1474
1561
  synchronize do |client|
1475
1562
  client.call([:zcard, key])
@@ -1500,38 +1587,27 @@ class Redis
1500
1587
  # - `:incr => true`: When this option is specified ZADD acts like
1501
1588
  # ZINCRBY; only one score-element pair can be specified in this mode
1502
1589
  #
1503
- # @return [Boolean, Fixnum, Float]
1590
+ # @return [Boolean, Integer, Float]
1504
1591
  # - `Boolean` when a single pair is specified, holding whether or not it was
1505
1592
  # **added** to the sorted set.
1506
- # - `Fixnum` when an array of pairs is specified, holding the number of
1593
+ # - `Integer` when an array of pairs is specified, holding the number of
1507
1594
  # pairs that were **added** to the sorted set.
1508
1595
  # - `Float` when option :incr is specified, holding the score of the member
1509
1596
  # after incrementing it.
1510
- def zadd(key, *args) #, options
1511
- zadd_options = []
1512
- if args.last.is_a?(Hash)
1513
- options = args.pop
1514
-
1515
- nx = options[:nx]
1516
- zadd_options << "NX" if nx
1517
-
1518
- xx = options[:xx]
1519
- zadd_options << "XX" if xx
1520
-
1521
- ch = options[:ch]
1522
- zadd_options << "CH" if ch
1523
-
1524
- incr = options[:incr]
1525
- zadd_options << "INCR" if incr
1526
- end
1597
+ def zadd(key, *args, nx: nil, xx: nil, ch: nil, incr: nil)
1598
+ command = [:zadd, key]
1599
+ command << "NX" if nx
1600
+ command << "XX" if xx
1601
+ command << "CH" if ch
1602
+ command << "INCR" if incr
1527
1603
 
1528
1604
  synchronize do |client|
1529
1605
  if args.size == 1 && args[0].is_a?(Array)
1530
1606
  # Variadic: return float if INCR, integer if !INCR
1531
- client.call([:zadd, key] + zadd_options + args[0], &(incr ? Floatify : nil))
1607
+ client.call(command + args[0], &(incr ? Floatify : nil))
1532
1608
  elsif args.size == 2
1533
1609
  # Single pair: return float if INCR, boolean if !INCR
1534
- client.call([:zadd, key] + zadd_options + args, &(incr ? Floatify : Boolify))
1610
+ client.call(command + args, &(incr ? Floatify : Boolify))
1535
1611
  else
1536
1612
  raise ArgumentError, "wrong number of arguments"
1537
1613
  end
@@ -1566,10 +1642,10 @@ class Redis
1566
1642
  # - a single member
1567
1643
  # - an array of members
1568
1644
  #
1569
- # @return [Boolean, Fixnum]
1645
+ # @return [Boolean, Integer]
1570
1646
  # - `Boolean` when a single member is specified, holding whether or not it
1571
1647
  # was removed from the sorted set
1572
- # - `Fixnum` when an array of pairs is specified, holding the number of
1648
+ # - `Integer` when an array of pairs is specified, holding the number of
1573
1649
  # members that were removed to the sorted set
1574
1650
  def zrem(key, member)
1575
1651
  synchronize do |client|
@@ -1585,6 +1661,90 @@ class Redis
1585
1661
  end
1586
1662
  end
1587
1663
 
1664
+ # Removes and returns up to count members with the highest scores in the sorted set stored at key.
1665
+ #
1666
+ # @example Popping a member
1667
+ # redis.zpopmax('zset')
1668
+ # #=> ['b', 2.0]
1669
+ # @example With count option
1670
+ # redis.zpopmax('zset', 2)
1671
+ # #=> [['b', 2.0], ['a', 1.0]]
1672
+ #
1673
+ # @params key [String] a key of the sorted set
1674
+ # @params count [Integer] a number of members
1675
+ #
1676
+ # @return [Array<String, Float>] element and score pair if count is not specified
1677
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1678
+ def zpopmax(key, count = nil)
1679
+ synchronize do |client|
1680
+ members = client.call([:zpopmax, key, count].compact, &FloatifyPairs)
1681
+ count.to_i > 1 ? members : members.first
1682
+ end
1683
+ end
1684
+
1685
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at key.
1686
+ #
1687
+ # @example Popping a member
1688
+ # redis.zpopmin('zset')
1689
+ # #=> ['a', 1.0]
1690
+ # @example With count option
1691
+ # redis.zpopmin('zset', 2)
1692
+ # #=> [['a', 1.0], ['b', 2.0]]
1693
+ #
1694
+ # @params key [String] a key of the sorted set
1695
+ # @params count [Integer] a number of members
1696
+ #
1697
+ # @return [Array<String, Float>] element and score pair if count is not specified
1698
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1699
+ def zpopmin(key, count = nil)
1700
+ synchronize do |client|
1701
+ members = client.call([:zpopmin, key, count].compact, &FloatifyPairs)
1702
+ count.to_i > 1 ? members : members.first
1703
+ end
1704
+ end
1705
+
1706
+ # Removes and returns up to count members with the highest scores in the sorted set stored at keys,
1707
+ # or block until one is available.
1708
+ #
1709
+ # @example Popping a member from a sorted set
1710
+ # redis.bzpopmax('zset', 1)
1711
+ # #=> ['zset', 'b', 2.0]
1712
+ # @example Popping a member from multiple sorted sets
1713
+ # redis.bzpopmax('zset1', 'zset2', 1)
1714
+ # #=> ['zset1', 'b', 2.0]
1715
+ #
1716
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1717
+ # @params timeout [Integer] the maximum number of seconds to block
1718
+ #
1719
+ # @return [Array<String, String, Float>] a touple of key, member and score
1720
+ # @return [nil] when no element could be popped and the timeout expired
1721
+ def bzpopmax(*args)
1722
+ _bpop(:bzpopmax, args) do |reply|
1723
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1724
+ end
1725
+ end
1726
+
1727
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at keys,
1728
+ # or block until one is available.
1729
+ #
1730
+ # @example Popping a member from a sorted set
1731
+ # redis.bzpopmin('zset', 1)
1732
+ # #=> ['zset', 'a', 1.0]
1733
+ # @example Popping a member from multiple sorted sets
1734
+ # redis.bzpopmin('zset1', 'zset2', 1)
1735
+ # #=> ['zset1', 'a', 1.0]
1736
+ #
1737
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1738
+ # @params timeout [Integer] the maximum number of seconds to block
1739
+ #
1740
+ # @return [Array<String, String, Float>] a touple of key, member and score
1741
+ # @return [nil] when no element could be popped and the timeout expired
1742
+ def bzpopmin(*args)
1743
+ _bpop(:bzpopmin, args) do |reply|
1744
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1745
+ end
1746
+ end
1747
+
1588
1748
  # Get the score associated with the given member in a sorted set.
1589
1749
  #
1590
1750
  # @example Get the score for member "a"
@@ -1610,18 +1770,16 @@ class Redis
1610
1770
  # # => [["a", 32.0], ["b", 64.0]]
1611
1771
  #
1612
1772
  # @param [String] key
1613
- # @param [Fixnum] start start index
1614
- # @param [Fixnum] stop stop index
1773
+ # @param [Integer] start start index
1774
+ # @param [Integer] stop stop index
1615
1775
  # @param [Hash] options
1616
1776
  # - `:with_scores => true`: include scores in output
1617
1777
  #
1618
1778
  # @return [Array<String>, Array<[String, Float]>]
1619
1779
  # - when `:with_scores` is not specified, an array of members
1620
1780
  # - when `:with_scores` is specified, an array with `[member, score]` pairs
1621
- def zrange(key, start, stop, options = {})
1622
- args = []
1623
-
1624
- with_scores = options[:with_scores] || options[:withscores]
1781
+ def zrange(key, start, stop, withscores: false, with_scores: withscores)
1782
+ args = [:zrange, key, start, stop]
1625
1783
 
1626
1784
  if with_scores
1627
1785
  args << "WITHSCORES"
@@ -1629,7 +1787,7 @@ class Redis
1629
1787
  end
1630
1788
 
1631
1789
  synchronize do |client|
1632
- client.call([:zrange, key, start, stop] + args, &block)
1790
+ client.call(args, &block)
1633
1791
  end
1634
1792
  end
1635
1793
 
@@ -1644,10 +1802,8 @@ class Redis
1644
1802
  # # => [["b", 64.0], ["a", 32.0]]
1645
1803
  #
1646
1804
  # @see #zrange
1647
- def zrevrange(key, start, stop, options = {})
1648
- args = []
1649
-
1650
- with_scores = options[:with_scores] || options[:withscores]
1805
+ def zrevrange(key, start, stop, withscores: false, with_scores: withscores)
1806
+ args = [:zrevrange, key, start, stop]
1651
1807
 
1652
1808
  if with_scores
1653
1809
  args << "WITHSCORES"
@@ -1655,7 +1811,7 @@ class Redis
1655
1811
  end
1656
1812
 
1657
1813
  synchronize do |client|
1658
- client.call([:zrevrange, key, start, stop] + args, &block)
1814
+ client.call(args, &block)
1659
1815
  end
1660
1816
  end
1661
1817
 
@@ -1663,7 +1819,7 @@ class Redis
1663
1819
  #
1664
1820
  # @param [String] key
1665
1821
  # @param [String] member
1666
- # @return [Fixnum]
1822
+ # @return [Integer]
1667
1823
  def zrank(key, member)
1668
1824
  synchronize do |client|
1669
1825
  client.call([:zrank, key, member])
@@ -1675,7 +1831,7 @@ class Redis
1675
1831
  #
1676
1832
  # @param [String] key
1677
1833
  # @param [String] member
1678
- # @return [Fixnum]
1834
+ # @return [Integer]
1679
1835
  def zrevrank(key, member)
1680
1836
  synchronize do |client|
1681
1837
  client.call([:zrevrank, key, member])
@@ -1692,15 +1848,39 @@ class Redis
1692
1848
  # # => 5
1693
1849
  #
1694
1850
  # @param [String] key
1695
- # @param [Fixnum] start start index
1696
- # @param [Fixnum] stop stop index
1697
- # @return [Fixnum] number of members that were removed
1851
+ # @param [Integer] start start index
1852
+ # @param [Integer] stop stop index
1853
+ # @return [Integer] number of members that were removed
1698
1854
  def zremrangebyrank(key, start, stop)
1699
1855
  synchronize do |client|
1700
1856
  client.call([:zremrangebyrank, key, start, stop])
1701
1857
  end
1702
1858
  end
1703
1859
 
1860
+ # Count the members, with the same score in a sorted set, within the given lexicographical range.
1861
+ #
1862
+ # @example Count members matching a
1863
+ # redis.zlexcount("zset", "[a", "[a\xff")
1864
+ # # => 1
1865
+ # @example Count members matching a-z
1866
+ # redis.zlexcount("zset", "[a", "[z\xff")
1867
+ # # => 26
1868
+ #
1869
+ # @param [String] key
1870
+ # @param [String] min
1871
+ # - inclusive minimum is specified by prefixing `(`
1872
+ # - exclusive minimum is specified by prefixing `[`
1873
+ # @param [String] max
1874
+ # - inclusive maximum is specified by prefixing `(`
1875
+ # - exclusive maximum is specified by prefixing `[`
1876
+ #
1877
+ # @return [Integer] number of members within the specified lexicographical range
1878
+ def zlexcount(key, min, max)
1879
+ synchronize do |client|
1880
+ client.call([:zlexcount, key, min, max])
1881
+ end
1882
+ end
1883
+
1704
1884
  # Return a range of members with the same score in a sorted set, by lexicographical ordering
1705
1885
  #
1706
1886
  # @example Retrieve members matching a
@@ -1722,14 +1902,16 @@ class Redis
1722
1902
  # `count` members
1723
1903
  #
1724
1904
  # @return [Array<String>, Array<[String, Float]>]
1725
- def zrangebylex(key, min, max, options = {})
1726
- args = []
1905
+ def zrangebylex(key, min, max, limit: nil)
1906
+ args = [:zrangebylex, key, min, max]
1727
1907
 
1728
- limit = options[:limit]
1729
- args.concat(["LIMIT"] + limit) if limit
1908
+ if limit
1909
+ args << "LIMIT"
1910
+ args.concat(limit)
1911
+ end
1730
1912
 
1731
1913
  synchronize do |client|
1732
- client.call([:zrangebylex, key, min, max] + args)
1914
+ client.call(args)
1733
1915
  end
1734
1916
  end
1735
1917
 
@@ -1744,14 +1926,16 @@ class Redis
1744
1926
  # # => ["abbygail", "abby"]
1745
1927
  #
1746
1928
  # @see #zrangebylex
1747
- def zrevrangebylex(key, max, min, options = {})
1748
- args = []
1929
+ def zrevrangebylex(key, max, min, limit: nil)
1930
+ args = [:zrevrangebylex, key, max, min]
1749
1931
 
1750
- limit = options[:limit]
1751
- args.concat(["LIMIT"] + limit) if limit
1932
+ if limit
1933
+ args << "LIMIT"
1934
+ args.concat(limit)
1935
+ end
1752
1936
 
1753
1937
  synchronize do |client|
1754
- client.call([:zrevrangebylex, key, max, min] + args)
1938
+ client.call(args)
1755
1939
  end
1756
1940
  end
1757
1941
 
@@ -1782,21 +1966,21 @@ class Redis
1782
1966
  # @return [Array<String>, Array<[String, Float]>]
1783
1967
  # - when `:with_scores` is not specified, an array of members
1784
1968
  # - when `:with_scores` is specified, an array with `[member, score]` pairs
1785
- def zrangebyscore(key, min, max, options = {})
1786
- args = []
1787
-
1788
- with_scores = options[:with_scores] || options[:withscores]
1969
+ def zrangebyscore(key, min, max, withscores: false, with_scores: withscores, limit: nil)
1970
+ args = [:zrangebyscore, key, min, max]
1789
1971
 
1790
1972
  if with_scores
1791
1973
  args << "WITHSCORES"
1792
1974
  block = FloatifyPairs
1793
1975
  end
1794
1976
 
1795
- limit = options[:limit]
1796
- args.concat(["LIMIT"] + limit) if limit
1977
+ if limit
1978
+ args << "LIMIT"
1979
+ args.concat(limit)
1980
+ end
1797
1981
 
1798
1982
  synchronize do |client|
1799
- client.call([:zrangebyscore, key, min, max] + args, &block)
1983
+ client.call(args, &block)
1800
1984
  end
1801
1985
  end
1802
1986
 
@@ -1814,21 +1998,21 @@ class Redis
1814
1998
  # # => [["b", 64.0], ["a", 32.0]]
1815
1999
  #
1816
2000
  # @see #zrangebyscore
1817
- def zrevrangebyscore(key, max, min, options = {})
1818
- args = []
1819
-
1820
- with_scores = options[:with_scores] || options[:withscores]
2001
+ def zrevrangebyscore(key, max, min, withscores: false, with_scores: withscores, limit: nil)
2002
+ args = [:zrevrangebyscore, key, max, min]
1821
2003
 
1822
2004
  if with_scores
1823
- args << ["WITHSCORES"]
2005
+ args << "WITHSCORES"
1824
2006
  block = FloatifyPairs
1825
2007
  end
1826
2008
 
1827
- limit = options[:limit]
1828
- args.concat(["LIMIT"] + limit) if limit
2009
+ if limit
2010
+ args << "LIMIT"
2011
+ args.concat(limit)
2012
+ end
1829
2013
 
1830
2014
  synchronize do |client|
1831
- client.call([:zrevrangebyscore, key, max, min] + args, &block)
2015
+ client.call(args, &block)
1832
2016
  end
1833
2017
  end
1834
2018
 
@@ -1848,7 +2032,7 @@ class Redis
1848
2032
  # @param [String] max
1849
2033
  # - inclusive maximum score is specified verbatim
1850
2034
  # - exclusive maximum score is specified by prefixing `(`
1851
- # @return [Fixnum] number of members that were removed
2035
+ # @return [Integer] number of members that were removed
1852
2036
  def zremrangebyscore(key, min, max)
1853
2037
  synchronize do |client|
1854
2038
  client.call([:zremrangebyscore, key, min, max])
@@ -1871,7 +2055,7 @@ class Redis
1871
2055
  # @param [String] max
1872
2056
  # - inclusive maximum score is specified verbatim
1873
2057
  # - exclusive maximum score is specified by prefixing `(`
1874
- # @return [Fixnum] number of members in within the specified range
2058
+ # @return [Integer] number of members in within the specified range
1875
2059
  def zcount(key, min, max)
1876
2060
  synchronize do |client|
1877
2061
  client.call([:zcount, key, min, max])
@@ -1891,18 +2075,19 @@ class Redis
1891
2075
  # - `:weights => [Float, Float, ...]`: weights to associate with source
1892
2076
  # sorted sets
1893
2077
  # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
1894
- # @return [Fixnum] number of elements in the resulting sorted set
1895
- def zinterstore(destination, keys, options = {})
1896
- args = []
2078
+ # @return [Integer] number of elements in the resulting sorted set
2079
+ def zinterstore(destination, keys, weights: nil, aggregate: nil)
2080
+ args = [:zinterstore, destination, keys.size, *keys]
1897
2081
 
1898
- weights = options[:weights]
1899
- args.concat(["WEIGHTS"] + weights) if weights
2082
+ if weights
2083
+ args << "WEIGHTS"
2084
+ args.concat(weights)
2085
+ end
1900
2086
 
1901
- aggregate = options[:aggregate]
1902
- args.concat(["AGGREGATE", aggregate]) if aggregate
2087
+ args << "AGGREGATE" << aggregate if aggregate
1903
2088
 
1904
2089
  synchronize do |client|
1905
- client.call([:zinterstore, destination, keys.size] + keys + args)
2090
+ client.call(args)
1906
2091
  end
1907
2092
  end
1908
2093
 
@@ -1918,40 +2103,46 @@ class Redis
1918
2103
  # - `:weights => [Float, Float, ...]`: weights to associate with source
1919
2104
  # sorted sets
1920
2105
  # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
1921
- # @return [Fixnum] number of elements in the resulting sorted set
1922
- def zunionstore(destination, keys, options = {})
1923
- args = []
2106
+ # @return [Integer] number of elements in the resulting sorted set
2107
+ def zunionstore(destination, keys, weights: nil, aggregate: nil)
2108
+ args = [:zunionstore, destination, keys.size, *keys]
1924
2109
 
1925
- weights = options[:weights]
1926
- args.concat(["WEIGHTS"] + weights) if weights
2110
+ if weights
2111
+ args << "WEIGHTS"
2112
+ args.concat(weights)
2113
+ end
1927
2114
 
1928
- aggregate = options[:aggregate]
1929
- args.concat(["AGGREGATE", aggregate]) if aggregate
2115
+ args << "AGGREGATE" << aggregate if aggregate
1930
2116
 
1931
2117
  synchronize do |client|
1932
- client.call([:zunionstore, destination, keys.size] + keys + args)
2118
+ client.call(args)
1933
2119
  end
1934
2120
  end
1935
2121
 
1936
2122
  # Get the number of fields in a hash.
1937
2123
  #
1938
2124
  # @param [String] key
1939
- # @return [Fixnum] number of fields in the hash
2125
+ # @return [Integer] number of fields in the hash
1940
2126
  def hlen(key)
1941
2127
  synchronize do |client|
1942
2128
  client.call([:hlen, key])
1943
2129
  end
1944
2130
  end
1945
2131
 
1946
- # Set the string value of a hash field.
2132
+ # Set one or more hash values.
2133
+ #
2134
+ # @example
2135
+ # redis.hset("hash", "f1", "v1", "f2", "v2") # => 2
2136
+ # redis.hset("hash", { "f1" => "v1", "f2" => "v2" }) # => 2
1947
2137
  #
1948
2138
  # @param [String] key
1949
- # @param [String] field
1950
- # @param [String] value
1951
- # @return [Boolean] whether or not the field was **added** to the hash
1952
- def hset(key, field, value)
2139
+ # @param [Array<String> | Hash<String, String>] attrs array or hash of fields and values
2140
+ # @return [Integer] The number of fields that were added to the hash
2141
+ def hset(key, *attrs)
2142
+ attrs = attrs.first.flatten if attrs.size == 1 && attrs.first.is_a?(Hash)
2143
+
1953
2144
  synchronize do |client|
1954
- client.call([:hset, key, field, value], &Boolify)
2145
+ client.call([:hset, key, *attrs])
1955
2146
  end
1956
2147
  end
1957
2148
 
@@ -2040,7 +2231,7 @@ class Redis
2040
2231
  # @see #hmget
2041
2232
  def mapped_hmget(key, *fields)
2042
2233
  hmget(key, *fields) do |reply|
2043
- if reply.kind_of?(Array)
2234
+ if reply.is_a?(Array)
2044
2235
  Hash[fields.zip(reply)]
2045
2236
  else
2046
2237
  reply
@@ -2052,10 +2243,10 @@ class Redis
2052
2243
  #
2053
2244
  # @param [String] key
2054
2245
  # @param [String, Array<String>] field
2055
- # @return [Fixnum] the number of fields that were removed from the hash
2056
- def hdel(key, field)
2246
+ # @return [Integer] the number of fields that were removed from the hash
2247
+ def hdel(key, *fields)
2057
2248
  synchronize do |client|
2058
- client.call([:hdel, key, field])
2249
+ client.call([:hdel, key, *fields])
2059
2250
  end
2060
2251
  end
2061
2252
 
@@ -2074,8 +2265,8 @@ class Redis
2074
2265
  #
2075
2266
  # @param [String] key
2076
2267
  # @param [String] field
2077
- # @param [Fixnum] increment
2078
- # @return [Fixnum] value of the field after incrementing it
2268
+ # @param [Integer] increment
2269
+ # @return [Integer] value of the field after incrementing it
2079
2270
  def hincrby(key, field, increment)
2080
2271
  synchronize do |client|
2081
2272
  client.call([:hincrby, key, field, increment])
@@ -2133,20 +2324,21 @@ class Redis
2133
2324
 
2134
2325
  def subscribed?
2135
2326
  synchronize do |client|
2136
- client.kind_of? SubscribedClient
2327
+ client.is_a? SubscribedClient
2137
2328
  end
2138
2329
  end
2139
2330
 
2140
2331
  # Listen for messages published to the given channels.
2141
2332
  def subscribe(*channels, &block)
2142
- synchronize do |client|
2333
+ synchronize do |_client|
2143
2334
  _subscription(:subscribe, 0, channels, block)
2144
2335
  end
2145
2336
  end
2146
2337
 
2147
- # Listen for messages published to the given channels. Throw a timeout error if there is no messages for a timeout period.
2338
+ # Listen for messages published to the given channels. Throw a timeout error
2339
+ # if there is no messages for a timeout period.
2148
2340
  def subscribe_with_timeout(timeout, *channels, &block)
2149
- synchronize do |client|
2341
+ synchronize do |_client|
2150
2342
  _subscription(:subscribe_with_timeout, timeout, channels, block)
2151
2343
  end
2152
2344
  end
@@ -2154,21 +2346,23 @@ class Redis
2154
2346
  # Stop listening for messages posted to the given channels.
2155
2347
  def unsubscribe(*channels)
2156
2348
  synchronize do |client|
2157
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2349
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2350
+
2158
2351
  client.unsubscribe(*channels)
2159
2352
  end
2160
2353
  end
2161
2354
 
2162
2355
  # Listen for messages published to channels matching the given patterns.
2163
2356
  def psubscribe(*channels, &block)
2164
- synchronize do |client|
2357
+ synchronize do |_client|
2165
2358
  _subscription(:psubscribe, 0, channels, block)
2166
2359
  end
2167
2360
  end
2168
2361
 
2169
- # Listen for messages published to channels matching the given patterns. Throw a timeout error if there is no messages for a timeout period.
2362
+ # Listen for messages published to channels matching the given patterns.
2363
+ # Throw a timeout error if there is no messages for a timeout period.
2170
2364
  def psubscribe_with_timeout(timeout, *channels, &block)
2171
- synchronize do |client|
2365
+ synchronize do |_client|
2172
2366
  _subscription(:psubscribe_with_timeout, timeout, channels, block)
2173
2367
  end
2174
2368
  end
@@ -2176,7 +2370,8 @@ class Redis
2176
2370
  # Stop listening for messages posted to channels matching the given patterns.
2177
2371
  def punsubscribe(*channels)
2178
2372
  synchronize do |client|
2179
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2373
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2374
+
2180
2375
  client.punsubscribe(*channels)
2181
2376
  end
2182
2377
  end
@@ -2221,7 +2416,7 @@ class Redis
2221
2416
  # @see #multi
2222
2417
  def watch(*keys)
2223
2418
  synchronize do |client|
2224
- res = client.call([:watch] + keys)
2419
+ res = client.call([:watch, *keys])
2225
2420
 
2226
2421
  if block_given?
2227
2422
  begin
@@ -2251,13 +2446,13 @@ class Redis
2251
2446
  end
2252
2447
 
2253
2448
  def pipelined
2254
- synchronize do |client|
2449
+ synchronize do |prior_client|
2255
2450
  begin
2256
- original, @client = @client, Pipeline.new
2451
+ @client = Pipeline.new(prior_client)
2257
2452
  yield(self)
2258
- original.call_pipeline(@client)
2453
+ prior_client.call_pipeline(@client)
2259
2454
  ensure
2260
- @client = original
2455
+ @client = prior_client
2261
2456
  end
2262
2457
  end
2263
2458
  end
@@ -2293,17 +2488,16 @@ class Redis
2293
2488
  # @see #watch
2294
2489
  # @see #unwatch
2295
2490
  def multi
2296
- synchronize do |client|
2491
+ synchronize do |prior_client|
2297
2492
  if !block_given?
2298
- client.call([:multi])
2493
+ prior_client.call([:multi])
2299
2494
  else
2300
2495
  begin
2301
- pipeline = Pipeline::Multi.new
2302
- original, @client = @client, pipeline
2496
+ @client = Pipeline::Multi.new(prior_client)
2303
2497
  yield(self)
2304
- original.call_pipeline(pipeline)
2498
+ prior_client.call_pipeline(@client)
2305
2499
  ensure
2306
- @client = original
2500
+ @client = prior_client
2307
2501
  end
2308
2502
  end
2309
2503
  end
@@ -2450,18 +2644,13 @@ class Redis
2450
2644
  _eval(:evalsha, args)
2451
2645
  end
2452
2646
 
2453
- def _scan(command, cursor, args, options = {}, &block)
2647
+ def _scan(command, cursor, args, match: nil, count: nil, type: nil, &block)
2454
2648
  # SSCAN/ZSCAN/HSCAN already prepend the key to +args+.
2455
2649
 
2456
2650
  args << cursor
2457
-
2458
- if match = options[:match]
2459
- args.concat(["MATCH", match])
2460
- end
2461
-
2462
- if count = options[:count]
2463
- args.concat(["COUNT", count])
2464
- end
2651
+ args << "MATCH" << match if match
2652
+ args << "COUNT" << count if count
2653
+ args << "TYPE" << type if type
2465
2654
 
2466
2655
  synchronize do |client|
2467
2656
  client.call([command] + args, &block)
@@ -2476,15 +2665,19 @@ class Redis
2476
2665
  # @example Retrieve a batch of keys matching a pattern
2477
2666
  # redis.scan(4, :match => "key:1?")
2478
2667
  # # => ["92", ["key:13", "key:18"]]
2668
+ # @example Retrieve a batch of keys of a certain type
2669
+ # redis.scan(92, :type => "zset")
2670
+ # # => ["173", ["sortedset:14", "sortedset:78"]]
2479
2671
  #
2480
2672
  # @param [String, Integer] cursor the cursor of the iteration
2481
2673
  # @param [Hash] options
2482
2674
  # - `:match => String`: only return keys matching the pattern
2483
2675
  # - `:count => Integer`: return count keys at most per iteration
2676
+ # - `:type => String`: return keys only of the given type
2484
2677
  #
2485
2678
  # @return [String, Array<String>] the next cursor and all found keys
2486
- def scan(cursor, options={})
2487
- _scan(:scan, cursor, [], options)
2679
+ def scan(cursor, **options)
2680
+ _scan(:scan, cursor, [], **options)
2488
2681
  end
2489
2682
 
2490
2683
  # Scan the keyspace
@@ -2496,17 +2689,23 @@ class Redis
2496
2689
  # redis.scan_each(:match => "key:1?") {|key| puts key}
2497
2690
  # # => key:13
2498
2691
  # # => key:18
2692
+ # @example Execute block for each key of a type
2693
+ # redis.scan_each(:type => "hash") {|key| puts redis.type(key)}
2694
+ # # => "hash"
2695
+ # # => "hash"
2499
2696
  #
2500
2697
  # @param [Hash] options
2501
2698
  # - `:match => String`: only return keys matching the pattern
2502
2699
  # - `:count => Integer`: return count keys at most per iteration
2700
+ # - `:type => String`: return keys only of the given type
2503
2701
  #
2504
2702
  # @return [Enumerator] an enumerator for all found keys
2505
- def scan_each(options={}, &block)
2506
- return to_enum(:scan_each, options) unless block_given?
2703
+ def scan_each(**options, &block)
2704
+ return to_enum(:scan_each, **options) unless block_given?
2705
+
2507
2706
  cursor = 0
2508
2707
  loop do
2509
- cursor, keys = scan(cursor, options)
2708
+ cursor, keys = scan(cursor, **options)
2510
2709
  keys.each(&block)
2511
2710
  break if cursor == "0"
2512
2711
  end
@@ -2523,8 +2722,8 @@ class Redis
2523
2722
  # - `:count => Integer`: return count keys at most per iteration
2524
2723
  #
2525
2724
  # @return [String, Array<[String, String]>] the next cursor and all found keys
2526
- def hscan(key, cursor, options={})
2527
- _scan(:hscan, cursor, [key], options) do |reply|
2725
+ def hscan(key, cursor, **options)
2726
+ _scan(:hscan, cursor, [key], **options) do |reply|
2528
2727
  [reply[0], reply[1].each_slice(2).to_a]
2529
2728
  end
2530
2729
  end
@@ -2540,11 +2739,12 @@ class Redis
2540
2739
  # - `:count => Integer`: return count keys at most per iteration
2541
2740
  #
2542
2741
  # @return [Enumerator] an enumerator for all found keys
2543
- def hscan_each(key, options={}, &block)
2544
- return to_enum(:hscan_each, key, options) unless block_given?
2742
+ def hscan_each(key, **options, &block)
2743
+ return to_enum(:hscan_each, key, **options) unless block_given?
2744
+
2545
2745
  cursor = 0
2546
2746
  loop do
2547
- cursor, values = hscan(key, cursor, options)
2747
+ cursor, values = hscan(key, cursor, **options)
2548
2748
  values.each(&block)
2549
2749
  break if cursor == "0"
2550
2750
  end
@@ -2562,8 +2762,8 @@ class Redis
2562
2762
  #
2563
2763
  # @return [String, Array<[String, Float]>] the next cursor and all found
2564
2764
  # members and scores
2565
- def zscan(key, cursor, options={})
2566
- _scan(:zscan, cursor, [key], options) do |reply|
2765
+ def zscan(key, cursor, **options)
2766
+ _scan(:zscan, cursor, [key], **options) do |reply|
2567
2767
  [reply[0], FloatifyPairs.call(reply[1])]
2568
2768
  end
2569
2769
  end
@@ -2579,11 +2779,12 @@ class Redis
2579
2779
  # - `:count => Integer`: return count keys at most per iteration
2580
2780
  #
2581
2781
  # @return [Enumerator] an enumerator for all found scores and members
2582
- def zscan_each(key, options={}, &block)
2583
- return to_enum(:zscan_each, key, options) unless block_given?
2782
+ def zscan_each(key, **options, &block)
2783
+ return to_enum(:zscan_each, key, **options) unless block_given?
2784
+
2584
2785
  cursor = 0
2585
2786
  loop do
2586
- cursor, values = zscan(key, cursor, options)
2787
+ cursor, values = zscan(key, cursor, **options)
2587
2788
  values.each(&block)
2588
2789
  break if cursor == "0"
2589
2790
  end
@@ -2600,8 +2801,8 @@ class Redis
2600
2801
  # - `:count => Integer`: return count keys at most per iteration
2601
2802
  #
2602
2803
  # @return [String, Array<String>] the next cursor and all found members
2603
- def sscan(key, cursor, options={})
2604
- _scan(:sscan, cursor, [key], options)
2804
+ def sscan(key, cursor, **options)
2805
+ _scan(:sscan, cursor, [key], **options)
2605
2806
  end
2606
2807
 
2607
2808
  # Scan a set
@@ -2615,11 +2816,12 @@ class Redis
2615
2816
  # - `:count => Integer`: return count keys at most per iteration
2616
2817
  #
2617
2818
  # @return [Enumerator] an enumerator for all keys in the set
2618
- def sscan_each(key, options={}, &block)
2619
- return to_enum(:sscan_each, key, options) unless block_given?
2819
+ def sscan_each(key, **options, &block)
2820
+ return to_enum(:sscan_each, key, **options) unless block_given?
2821
+
2620
2822
  cursor = 0
2621
2823
  loop do
2622
- cursor, keys = sscan(key, cursor, options)
2824
+ cursor, keys = sscan(key, cursor, **options)
2623
2825
  keys.each(&block)
2624
2826
  break if cursor == "0"
2625
2827
  end
@@ -2642,7 +2844,7 @@ class Redis
2642
2844
  # union of the HyperLogLogs contained in the keys.
2643
2845
  #
2644
2846
  # @param [String, Array<String>] keys
2645
- # @return [Fixnum]
2847
+ # @return [Integer]
2646
2848
  def pfcount(*keys)
2647
2849
  synchronize do |client|
2648
2850
  client.call([:pfcount] + keys)
@@ -2661,6 +2863,413 @@ class Redis
2661
2863
  end
2662
2864
  end
2663
2865
 
2866
+ # Adds the specified geospatial items (latitude, longitude, name) to the specified key
2867
+ #
2868
+ # @param [String] key
2869
+ # @param [Array] member arguemnts for member or members: longitude, latitude, name
2870
+ # @return [Integer] number of elements added to the sorted set
2871
+ def geoadd(key, *member)
2872
+ synchronize do |client|
2873
+ client.call([:geoadd, key, *member])
2874
+ end
2875
+ end
2876
+
2877
+ # Returns geohash string representing position for specified members of the specified key.
2878
+ #
2879
+ # @param [String] key
2880
+ # @param [String, Array<String>] member one member or array of members
2881
+ # @return [Array<String, nil>] returns array containg geohash string if member is present, nil otherwise
2882
+ def geohash(key, member)
2883
+ synchronize do |client|
2884
+ client.call([:geohash, key, member])
2885
+ end
2886
+ end
2887
+
2888
+ # Query a sorted set representing a geospatial index to fetch members matching a
2889
+ # given maximum distance from a point
2890
+ #
2891
+ # @param [Array] args key, longitude, latitude, radius, unit(m|km|ft|mi)
2892
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest
2893
+ # or the farthest to the nearest relative to the center
2894
+ # @param [Integer] count limit the results to the first N matching items
2895
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2896
+ # @return [Array<String>] may be changed with `options`
2897
+
2898
+ def georadius(*args, **geoptions)
2899
+ geoarguments = _geoarguments(*args, **geoptions)
2900
+
2901
+ synchronize do |client|
2902
+ client.call([:georadius, *geoarguments])
2903
+ end
2904
+ end
2905
+
2906
+ # Query a sorted set representing a geospatial index to fetch members matching a
2907
+ # given maximum distance from an already existing member
2908
+ #
2909
+ # @param [Array] args key, member, radius, unit(m|km|ft|mi)
2910
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest
2911
+ # to the nearest relative to the center
2912
+ # @param [Integer] count limit the results to the first N matching items
2913
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2914
+ # @return [Array<String>] may be changed with `options`
2915
+
2916
+ def georadiusbymember(*args, **geoptions)
2917
+ geoarguments = _geoarguments(*args, **geoptions)
2918
+
2919
+ synchronize do |client|
2920
+ client.call([:georadiusbymember, *geoarguments])
2921
+ end
2922
+ end
2923
+
2924
+ # Returns longitude and latitude of members of a geospatial index
2925
+ #
2926
+ # @param [String] key
2927
+ # @param [String, Array<String>] member one member or array of members
2928
+ # @return [Array<Array<String>, nil>] returns array of elements, where each
2929
+ # element is either array of longitude and latitude or nil
2930
+ def geopos(key, member)
2931
+ synchronize do |client|
2932
+ client.call([:geopos, key, member])
2933
+ end
2934
+ end
2935
+
2936
+ # Returns the distance between two members of a geospatial index
2937
+ #
2938
+ # @param [String ]key
2939
+ # @param [Array<String>] members
2940
+ # @param ['m', 'km', 'mi', 'ft'] unit
2941
+ # @return [String, nil] returns distance in spefied unit if both members present, nil otherwise.
2942
+ def geodist(key, member1, member2, unit = 'm')
2943
+ synchronize do |client|
2944
+ client.call([:geodist, key, member1, member2, unit])
2945
+ end
2946
+ end
2947
+
2948
+ # Returns the stream information each subcommand.
2949
+ #
2950
+ # @example stream
2951
+ # redis.xinfo(:stream, 'mystream')
2952
+ # @example groups
2953
+ # redis.xinfo(:groups, 'mystream')
2954
+ # @example consumers
2955
+ # redis.xinfo(:consumers, 'mystream', 'mygroup')
2956
+ #
2957
+ # @param subcommand [String] e.g. `stream` `groups` `consumers`
2958
+ # @param key [String] the stream key
2959
+ # @param group [String] the consumer group name, required if subcommand is `consumers`
2960
+ #
2961
+ # @return [Hash] information of the stream if subcommand is `stream`
2962
+ # @return [Array<Hash>] information of the consumer groups if subcommand is `groups`
2963
+ # @return [Array<Hash>] information of the consumers if subcommand is `consumers`
2964
+ def xinfo(subcommand, key, group = nil)
2965
+ args = [:xinfo, subcommand, key, group].compact
2966
+ synchronize do |client|
2967
+ client.call(args) do |reply|
2968
+ case subcommand.to_s.downcase
2969
+ when 'stream' then Hashify.call(reply)
2970
+ when 'groups', 'consumers' then reply.map { |arr| Hashify.call(arr) }
2971
+ else reply
2972
+ end
2973
+ end
2974
+ end
2975
+ end
2976
+
2977
+ # Add new entry to the stream.
2978
+ #
2979
+ # @example Without options
2980
+ # redis.xadd('mystream', f1: 'v1', f2: 'v2')
2981
+ # @example With options
2982
+ # redis.xadd('mystream', { f1: 'v1', f2: 'v2' }, id: '0-0', maxlen: 1000, approximate: true)
2983
+ #
2984
+ # @param key [String] the stream key
2985
+ # @param entry [Hash] one or multiple field-value pairs
2986
+ # @param opts [Hash] several options for `XADD` command
2987
+ #
2988
+ # @option opts [String] :id the entry id, default value is `*`, it means auto generation
2989
+ # @option opts [Integer] :maxlen max length of entries
2990
+ # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen or not
2991
+ #
2992
+ # @return [String] the entry id
2993
+ def xadd(key, entry, approximate: nil, maxlen: nil, id: '*')
2994
+ args = [:xadd, key]
2995
+ if maxlen
2996
+ args << "MAXLEN"
2997
+ args << "~" if approximate
2998
+ args << maxlen
2999
+ end
3000
+ args << id
3001
+ args.concat(entry.to_a.flatten)
3002
+ synchronize { |client| client.call(args) }
3003
+ end
3004
+
3005
+ # Trims older entries of the stream if needed.
3006
+ #
3007
+ # @example Without options
3008
+ # redis.xtrim('mystream', 1000)
3009
+ # @example With options
3010
+ # redis.xtrim('mystream', 1000, approximate: true)
3011
+ #
3012
+ # @param key [String] the stream key
3013
+ # @param mexlen [Integer] max length of entries
3014
+ # @param approximate [Boolean] whether to add `~` modifier of maxlen or not
3015
+ #
3016
+ # @return [Integer] the number of entries actually deleted
3017
+ def xtrim(key, maxlen, approximate: false)
3018
+ args = [:xtrim, key, 'MAXLEN', (approximate ? '~' : nil), maxlen].compact
3019
+ synchronize { |client| client.call(args) }
3020
+ end
3021
+
3022
+ # Delete entries by entry ids.
3023
+ #
3024
+ # @example With splatted entry ids
3025
+ # redis.xdel('mystream', '0-1', '0-2')
3026
+ # @example With arrayed entry ids
3027
+ # redis.xdel('mystream', ['0-1', '0-2'])
3028
+ #
3029
+ # @param key [String] the stream key
3030
+ # @param ids [Array<String>] one or multiple entry ids
3031
+ #
3032
+ # @return [Integer] the number of entries actually deleted
3033
+ def xdel(key, *ids)
3034
+ args = [:xdel, key].concat(ids.flatten)
3035
+ synchronize { |client| client.call(args) }
3036
+ end
3037
+
3038
+ # Fetches entries of the stream in ascending order.
3039
+ #
3040
+ # @example Without options
3041
+ # redis.xrange('mystream')
3042
+ # @example With a specific start
3043
+ # redis.xrange('mystream', '0-1')
3044
+ # @example With a specific start and end
3045
+ # redis.xrange('mystream', '0-1', '0-3')
3046
+ # @example With count options
3047
+ # redis.xrange('mystream', count: 10)
3048
+ #
3049
+ # @param key [String] the stream key
3050
+ # @param start [String] first entry id of range, default value is `-`
3051
+ # @param end [String] last entry id of range, default value is `+`
3052
+ # @param count [Integer] the number of entries as limit
3053
+ #
3054
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3055
+ def xrange(key, start = '-', range_end = '+', count: nil)
3056
+ args = [:xrange, key, start, range_end]
3057
+ args.concat(['COUNT', count]) if count
3058
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3059
+ end
3060
+
3061
+ # Fetches entries of the stream in descending order.
3062
+ #
3063
+ # @example Without options
3064
+ # redis.xrevrange('mystream')
3065
+ # @example With a specific end
3066
+ # redis.xrevrange('mystream', '0-3')
3067
+ # @example With a specific end and start
3068
+ # redis.xrevrange('mystream', '0-3', '0-1')
3069
+ # @example With count options
3070
+ # redis.xrevrange('mystream', count: 10)
3071
+ #
3072
+ # @param key [String] the stream key
3073
+ # @param end [String] first entry id of range, default value is `+`
3074
+ # @param start [String] last entry id of range, default value is `-`
3075
+ # @params count [Integer] the number of entries as limit
3076
+ #
3077
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3078
+ def xrevrange(key, range_end = '+', start = '-', count: nil)
3079
+ args = [:xrevrange, key, range_end, start]
3080
+ args.concat(['COUNT', count]) if count
3081
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3082
+ end
3083
+
3084
+ # Returns the number of entries inside a stream.
3085
+ #
3086
+ # @example With key
3087
+ # redis.xlen('mystream')
3088
+ #
3089
+ # @param key [String] the stream key
3090
+ #
3091
+ # @return [Integer] the number of entries
3092
+ def xlen(key)
3093
+ synchronize { |client| client.call([:xlen, key]) }
3094
+ end
3095
+
3096
+ # Fetches entries from one or multiple streams. Optionally blocking.
3097
+ #
3098
+ # @example With a key
3099
+ # redis.xread('mystream', '0-0')
3100
+ # @example With multiple keys
3101
+ # redis.xread(%w[mystream1 mystream2], %w[0-0 0-0])
3102
+ # @example With count option
3103
+ # redis.xread('mystream', '0-0', count: 2)
3104
+ # @example With block option
3105
+ # redis.xread('mystream', '$', block: 1000)
3106
+ #
3107
+ # @param keys [Array<String>] one or multiple stream keys
3108
+ # @param ids [Array<String>] one or multiple entry ids
3109
+ # @param count [Integer] the number of entries as limit per stream
3110
+ # @param block [Integer] the number of milliseconds as blocking timeout
3111
+ #
3112
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3113
+ def xread(keys, ids, count: nil, block: nil)
3114
+ args = [:xread]
3115
+ args << 'COUNT' << count if count
3116
+ args << 'BLOCK' << block.to_i if block
3117
+ _xread(args, keys, ids, block)
3118
+ end
3119
+
3120
+ # Manages the consumer group of the stream.
3121
+ #
3122
+ # @example With `create` subcommand
3123
+ # redis.xgroup(:create, 'mystream', 'mygroup', '$')
3124
+ # @example With `setid` subcommand
3125
+ # redis.xgroup(:setid, 'mystream', 'mygroup', '$')
3126
+ # @example With `destroy` subcommand
3127
+ # redis.xgroup(:destroy, 'mystream', 'mygroup')
3128
+ # @example With `delconsumer` subcommand
3129
+ # redis.xgroup(:delconsumer, 'mystream', 'mygroup', 'consumer1')
3130
+ #
3131
+ # @param subcommand [String] `create` `setid` `destroy` `delconsumer`
3132
+ # @param key [String] the stream key
3133
+ # @param group [String] the consumer group name
3134
+ # @param id_or_consumer [String]
3135
+ # * the entry id or `$`, required if subcommand is `create` or `setid`
3136
+ # * the consumer name, required if subcommand is `delconsumer`
3137
+ # @param mkstream [Boolean] whether to create an empty stream automatically or not
3138
+ #
3139
+ # @return [String] `OK` if subcommand is `create` or `setid`
3140
+ # @return [Integer] effected count if subcommand is `destroy` or `delconsumer`
3141
+ def xgroup(subcommand, key, group, id_or_consumer = nil, mkstream: false)
3142
+ args = [:xgroup, subcommand, key, group, id_or_consumer, (mkstream ? 'MKSTREAM' : nil)].compact
3143
+ synchronize { |client| client.call(args) }
3144
+ end
3145
+
3146
+ # Fetches a subset of the entries from one or multiple streams related with the consumer group.
3147
+ # Optionally blocking.
3148
+ #
3149
+ # @example With a key
3150
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>')
3151
+ # @example With multiple keys
3152
+ # redis.xreadgroup('mygroup', 'consumer1', %w[mystream1 mystream2], %w[> >])
3153
+ # @example With count option
3154
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', count: 2)
3155
+ # @example With block option
3156
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', block: 1000)
3157
+ # @example With noack option
3158
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', noack: true)
3159
+ #
3160
+ # @param group [String] the consumer group name
3161
+ # @param consumer [String] the consumer name
3162
+ # @param keys [Array<String>] one or multiple stream keys
3163
+ # @param ids [Array<String>] one or multiple entry ids
3164
+ # @param opts [Hash] several options for `XREADGROUP` command
3165
+ #
3166
+ # @option opts [Integer] :count the number of entries as limit
3167
+ # @option opts [Integer] :block the number of milliseconds as blocking timeout
3168
+ # @option opts [Boolean] :noack whether message loss is acceptable or not
3169
+ #
3170
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3171
+ def xreadgroup(group, consumer, keys, ids, count: nil, block: nil, noack: nil)
3172
+ args = [:xreadgroup, 'GROUP', group, consumer]
3173
+ args << 'COUNT' << count if count
3174
+ args << 'BLOCK' << block.to_i if block
3175
+ args << 'NOACK' if noack
3176
+ _xread(args, keys, ids, block)
3177
+ end
3178
+
3179
+ # Removes one or multiple entries from the pending entries list of a stream consumer group.
3180
+ #
3181
+ # @example With a entry id
3182
+ # redis.xack('mystream', 'mygroup', '1526569495631-0')
3183
+ # @example With splatted entry ids
3184
+ # redis.xack('mystream', 'mygroup', '0-1', '0-2')
3185
+ # @example With arrayed entry ids
3186
+ # redis.xack('mystream', 'mygroup', %w[0-1 0-2])
3187
+ #
3188
+ # @param key [String] the stream key
3189
+ # @param group [String] the consumer group name
3190
+ # @param ids [Array<String>] one or multiple entry ids
3191
+ #
3192
+ # @return [Integer] the number of entries successfully acknowledged
3193
+ def xack(key, group, *ids)
3194
+ args = [:xack, key, group].concat(ids.flatten)
3195
+ synchronize { |client| client.call(args) }
3196
+ end
3197
+
3198
+ # Changes the ownership of a pending entry
3199
+ #
3200
+ # @example With splatted entry ids
3201
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-1', '0-2')
3202
+ # @example With arrayed entry ids
3203
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2])
3204
+ # @example With idle option
3205
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], idle: 1000)
3206
+ # @example With time option
3207
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], time: 1542866959000)
3208
+ # @example With retrycount option
3209
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], retrycount: 10)
3210
+ # @example With force option
3211
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], force: true)
3212
+ # @example With justid option
3213
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], justid: true)
3214
+ #
3215
+ # @param key [String] the stream key
3216
+ # @param group [String] the consumer group name
3217
+ # @param consumer [String] the consumer name
3218
+ # @param min_idle_time [Integer] the number of milliseconds
3219
+ # @param ids [Array<String>] one or multiple entry ids
3220
+ # @param opts [Hash] several options for `XCLAIM` command
3221
+ #
3222
+ # @option opts [Integer] :idle the number of milliseconds as last time it was delivered of the entry
3223
+ # @option opts [Integer] :time the number of milliseconds as a specific Unix Epoch time
3224
+ # @option opts [Integer] :retrycount the number of retry counter
3225
+ # @option opts [Boolean] :force whether to create the pending entry to the pending entries list or not
3226
+ # @option opts [Boolean] :justid whether to fetch just an array of entry ids or not
3227
+ #
3228
+ # @return [Hash{String => Hash}] the entries successfully claimed
3229
+ # @return [Array<String>] the entry ids successfully claimed if justid option is `true`
3230
+ def xclaim(key, group, consumer, min_idle_time, *ids, **opts)
3231
+ args = [:xclaim, key, group, consumer, min_idle_time].concat(ids.flatten)
3232
+ args.concat(['IDLE', opts[:idle].to_i]) if opts[:idle]
3233
+ args.concat(['TIME', opts[:time].to_i]) if opts[:time]
3234
+ args.concat(['RETRYCOUNT', opts[:retrycount]]) if opts[:retrycount]
3235
+ args << 'FORCE' if opts[:force]
3236
+ args << 'JUSTID' if opts[:justid]
3237
+ blk = opts[:justid] ? Noop : HashifyStreamEntries
3238
+ synchronize { |client| client.call(args, &blk) }
3239
+ end
3240
+
3241
+ # Fetches not acknowledging pending entries
3242
+ #
3243
+ # @example With key and group
3244
+ # redis.xpending('mystream', 'mygroup')
3245
+ # @example With range options
3246
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10)
3247
+ # @example With range and consumer options
3248
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10, 'consumer1')
3249
+ #
3250
+ # @param key [String] the stream key
3251
+ # @param group [String] the consumer group name
3252
+ # @param start [String] start first entry id of range
3253
+ # @param end [String] end last entry id of range
3254
+ # @param count [Integer] count the number of entries as limit
3255
+ # @param consumer [String] the consumer name
3256
+ #
3257
+ # @return [Hash] the summary of pending entries
3258
+ # @return [Array<Hash>] the pending entries details if options were specified
3259
+ def xpending(key, group, *args)
3260
+ command_args = [:xpending, key, group]
3261
+ case args.size
3262
+ when 0, 3, 4
3263
+ command_args.concat(args)
3264
+ else
3265
+ raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 2, 5 or 6)"
3266
+ end
3267
+
3268
+ summary_needed = args.empty?
3269
+ blk = summary_needed ? HashifyStreamPendings : HashifyStreamPendingDetails
3270
+ synchronize { |client| client.call(command_args, &blk) }
3271
+ end
3272
+
2664
3273
  # Interact with the sentinel command (masters, master, slaves, failover)
2665
3274
  #
2666
3275
  # @param [String] subcommand e.g. `masters`, `master`, `slaves`
@@ -2674,8 +3283,8 @@ class Redis
2674
3283
  when "get-master-addr-by-name"
2675
3284
  reply
2676
3285
  else
2677
- if reply.kind_of?(Array)
2678
- if reply[0].kind_of?(Array)
3286
+ if reply.is_a?(Array)
3287
+ if reply[0].is_a?(Array)
2679
3288
  reply.map(&Hashify)
2680
3289
  else
2681
3290
  Hashify.call(reply)
@@ -2688,6 +3297,46 @@ class Redis
2688
3297
  end
2689
3298
  end
2690
3299
 
3300
+ # Sends `CLUSTER *` command to random node and returns its reply.
3301
+ #
3302
+ # @see https://redis.io/commands#cluster Reference of cluster command
3303
+ #
3304
+ # @param subcommand [String, Symbol] the subcommand of cluster command
3305
+ # e.g. `:slots`, `:nodes`, `:slaves`, `:info`
3306
+ #
3307
+ # @return [Object] depends on the subcommand
3308
+ def cluster(subcommand, *args)
3309
+ subcommand = subcommand.to_s.downcase
3310
+ block = case subcommand
3311
+ when 'slots'
3312
+ HashifyClusterSlots
3313
+ when 'nodes'
3314
+ HashifyClusterNodes
3315
+ when 'slaves'
3316
+ HashifyClusterSlaves
3317
+ when 'info'
3318
+ HashifyInfo
3319
+ else
3320
+ Noop
3321
+ end
3322
+
3323
+ # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
3324
+ block = Noop unless @cluster_mode
3325
+
3326
+ synchronize do |client|
3327
+ client.call([:cluster, subcommand] + args, &block)
3328
+ end
3329
+ end
3330
+
3331
+ # Sends `ASKING` command to random node and returns its reply.
3332
+ #
3333
+ # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
3334
+ #
3335
+ # @return [String] `'OK'`
3336
+ def asking
3337
+ synchronize { |client| client.call(%i[asking]) }
3338
+ end
3339
+
2691
3340
  def id
2692
3341
  @original_client.id
2693
3342
  end
@@ -2701,68 +3350,169 @@ class Redis
2701
3350
  end
2702
3351
 
2703
3352
  def connection
3353
+ return @original_client.connection_info if @cluster_mode
3354
+
2704
3355
  {
2705
- :host => @original_client.host,
2706
- :port => @original_client.port,
2707
- :db => @original_client.db,
2708
- :id => @original_client.id,
2709
- :location => @original_client.location
3356
+ host: @original_client.host,
3357
+ port: @original_client.port,
3358
+ db: @original_client.db,
3359
+ id: @original_client.id,
3360
+ location: @original_client.location
2710
3361
  }
2711
3362
  end
2712
3363
 
2713
- def method_missing(command, *args)
3364
+ def method_missing(command, *args) # rubocop:disable Style/MissingRespondToMissing
2714
3365
  synchronize do |client|
2715
3366
  client.call([command] + args)
2716
3367
  end
2717
3368
  end
2718
3369
 
2719
- private
3370
+ private
2720
3371
 
2721
3372
  # Commands returning 1 for true and 0 for false may be executed in a pipeline
2722
3373
  # where the method call will return nil. Propagate the nil instead of falsely
2723
3374
  # returning false.
2724
- Boolify =
2725
- lambda { |value|
2726
- value == 1 if value
2727
- }
3375
+ Boolify = lambda { |value|
3376
+ case value
3377
+ when 1
3378
+ true
3379
+ when 0
3380
+ false
3381
+ else
3382
+ value
3383
+ end
3384
+ }
2728
3385
 
2729
- BoolifySet =
2730
- lambda { |value|
2731
- if value && "OK" == value
2732
- true
2733
- else
2734
- false
2735
- end
2736
- }
3386
+ BoolifySet = lambda { |value|
3387
+ case value
3388
+ when "OK"
3389
+ true
3390
+ when nil
3391
+ false
3392
+ else
3393
+ value
3394
+ end
3395
+ }
2737
3396
 
2738
- Hashify =
2739
- lambda { |array|
2740
- hash = Hash.new
2741
- array.each_slice(2) do |field, value|
2742
- hash[field] = value
2743
- end
2744
- hash
2745
- }
3397
+ Hashify = lambda { |value|
3398
+ if value.respond_to?(:each_slice)
3399
+ value.each_slice(2).to_h
3400
+ else
3401
+ value
3402
+ end
3403
+ }
3404
+
3405
+ Floatify = lambda { |value|
3406
+ case value
3407
+ when "inf"
3408
+ Float::INFINITY
3409
+ when "-inf"
3410
+ -Float::INFINITY
3411
+ when String
3412
+ Float(value)
3413
+ else
3414
+ value
3415
+ end
3416
+ }
2746
3417
 
2747
- Floatify =
2748
- lambda { |str|
2749
- if str
2750
- if (inf = str.match(/^(-)?inf/i))
2751
- (inf[1] ? -1.0 : 1.0) / 0.0
2752
- else
2753
- Float(str)
2754
- end
2755
- end
3418
+ FloatifyPairs = lambda { |value|
3419
+ return value unless value.respond_to?(:each_slice)
3420
+
3421
+ value.each_slice(2).map do |member, score|
3422
+ [member, Floatify.call(score)]
3423
+ end
3424
+ }
3425
+
3426
+ HashifyInfo = lambda { |reply|
3427
+ lines = reply.split("\r\n").grep_v(/^(#|$)/)
3428
+ lines.map! { |line| line.split(':', 2) }
3429
+ lines.compact!
3430
+ lines.to_h
3431
+ }
3432
+
3433
+ HashifyStreams = lambda { |reply|
3434
+ case reply
3435
+ when nil
3436
+ {}
3437
+ else
3438
+ reply.map { |key, entries| [key, HashifyStreamEntries.call(entries)] }.to_h
3439
+ end
3440
+ }
3441
+
3442
+ EMPTY_STREAM_RESPONSE = [nil].freeze
3443
+ private_constant :EMPTY_STREAM_RESPONSE
3444
+
3445
+ HashifyStreamEntries = lambda { |reply|
3446
+ reply.compact.map do |entry_id, values|
3447
+ [entry_id, values.each_slice(2).to_h]
3448
+ end
3449
+ }
3450
+
3451
+ HashifyStreamPendings = lambda { |reply|
3452
+ {
3453
+ 'size' => reply[0],
3454
+ 'min_entry_id' => reply[1],
3455
+ 'max_entry_id' => reply[2],
3456
+ 'consumers' => reply[3].nil? ? {} : reply[3].to_h
2756
3457
  }
3458
+ }
2757
3459
 
2758
- FloatifyPairs =
2759
- lambda { |array|
2760
- if array
2761
- array.each_slice(2).map do |member, score|
2762
- [member, Floatify.call(score)]
2763
- end
2764
- end
3460
+ HashifyStreamPendingDetails = lambda { |reply|
3461
+ reply.map do |arr|
3462
+ {
3463
+ 'entry_id' => arr[0],
3464
+ 'consumer' => arr[1],
3465
+ 'elapsed' => arr[2],
3466
+ 'count' => arr[3]
3467
+ }
3468
+ end
3469
+ }
3470
+
3471
+ HashifyClusterNodeInfo = lambda { |str|
3472
+ arr = str.split(' ')
3473
+ {
3474
+ 'node_id' => arr[0],
3475
+ 'ip_port' => arr[1],
3476
+ 'flags' => arr[2].split(','),
3477
+ 'master_node_id' => arr[3],
3478
+ 'ping_sent' => arr[4],
3479
+ 'pong_recv' => arr[5],
3480
+ 'config_epoch' => arr[6],
3481
+ 'link_state' => arr[7],
3482
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
2765
3483
  }
3484
+ }
3485
+
3486
+ HashifyClusterSlots = lambda { |reply|
3487
+ reply.map do |arr|
3488
+ first_slot, last_slot = arr[0..1]
3489
+ master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
3490
+ replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
3491
+ {
3492
+ 'start_slot' => first_slot,
3493
+ 'end_slot' => last_slot,
3494
+ 'master' => master,
3495
+ 'replicas' => replicas
3496
+ }
3497
+ end
3498
+ }
3499
+
3500
+ HashifyClusterNodes = lambda { |reply|
3501
+ reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
3502
+ }
3503
+
3504
+ HashifyClusterSlaves = lambda { |reply|
3505
+ reply.map { |str| HashifyClusterNodeInfo.call(str) }
3506
+ }
3507
+
3508
+ Noop = ->(reply) { reply }
3509
+
3510
+ def _geoarguments(*args, options: nil, sort: nil, count: nil)
3511
+ args.push sort if sort
3512
+ args.push 'count', count if count
3513
+ args.push options if options
3514
+ args
3515
+ end
2766
3516
 
2767
3517
  def _subscription(method, timeout, channels, block)
2768
3518
  return @client.call([method] + channels) if subscribed?
@@ -2779,10 +3529,29 @@ private
2779
3529
  end
2780
3530
  end
2781
3531
 
3532
+ def _xread(args, keys, ids, blocking_timeout_msec)
3533
+ keys = keys.is_a?(Array) ? keys : [keys]
3534
+ ids = ids.is_a?(Array) ? ids : [ids]
3535
+ args << 'STREAMS'
3536
+ args.concat(keys)
3537
+ args.concat(ids)
3538
+
3539
+ synchronize do |client|
3540
+ if blocking_timeout_msec.nil?
3541
+ client.call(args, &HashifyStreams)
3542
+ elsif blocking_timeout_msec.to_f.zero?
3543
+ client.call_without_timeout(args, &HashifyStreams)
3544
+ else
3545
+ timeout = client.timeout.to_f + blocking_timeout_msec.to_f / 1000.0
3546
+ client.call_with_timeout(args, timeout, &HashifyStreams)
3547
+ end
3548
+ end
3549
+ end
2782
3550
  end
2783
3551
 
2784
- require "redis/version"
2785
- require "redis/connection"
2786
- require "redis/client"
2787
- require "redis/pipeline"
2788
- require "redis/subscribe"
3552
+ require_relative "redis/version"
3553
+ require_relative "redis/connection"
3554
+ require_relative "redis/client"
3555
+ require_relative "redis/cluster"
3556
+ require_relative "redis/pipeline"
3557
+ require_relative "redis/subscribe"