redis 3.3.5 → 4.3.1

Sign up to get free protection for your applications and to get access to all the features.
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"