redis 3.3.5 → 4.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +161 -2
  3. data/README.md +144 -79
  4. data/lib/redis/client.rb +166 -90
  5. data/lib/redis/cluster/command.rb +81 -0
  6. data/lib/redis/cluster/command_loader.rb +33 -0
  7. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  8. data/lib/redis/cluster/node.rb +108 -0
  9. data/lib/redis/cluster/node_key.rb +31 -0
  10. data/lib/redis/cluster/node_loader.rb +37 -0
  11. data/lib/redis/cluster/option.rb +93 -0
  12. data/lib/redis/cluster/slot.rb +86 -0
  13. data/lib/redis/cluster/slot_loader.rb +49 -0
  14. data/lib/redis/cluster.rb +291 -0
  15. data/lib/redis/connection/command_helper.rb +7 -10
  16. data/lib/redis/connection/hiredis.rb +6 -5
  17. data/lib/redis/connection/registry.rb +2 -1
  18. data/lib/redis/connection/ruby.rb +128 -129
  19. data/lib/redis/connection/synchrony.rb +21 -8
  20. data/lib/redis/connection.rb +4 -2
  21. data/lib/redis/distributed.rb +194 -72
  22. data/lib/redis/errors.rb +48 -0
  23. data/lib/redis/hash_ring.rb +30 -73
  24. data/lib/redis/pipeline.rb +55 -15
  25. data/lib/redis/subscribe.rb +11 -12
  26. data/lib/redis/version.rb +3 -1
  27. data/lib/redis.rb +1451 -403
  28. metadata +49 -202
  29. data/.gitignore +0 -16
  30. data/.travis/Gemfile +0 -11
  31. data/.travis.yml +0 -89
  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/sentinel.conf +0 -9
  47. data/examples/sentinel/start +0 -49
  48. data/examples/sentinel.rb +0 -41
  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,68 @@
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
+ @exists_returns_integer = true
5
8
 
6
- def self.deprecate(message, trace = caller[0])
7
- $stderr.puts "\n#{message} (in #{trace})"
8
- end
9
+ class << self
10
+ attr_reader :exists_returns_integer
9
11
 
10
- attr :client
12
+ def exists_returns_integer=(value)
13
+ unless value
14
+ message = "`Redis#exists(key)` will return an Integer by default in redis-rb 4.3. The option to explicitly " \
15
+ "disable this behaviour via `Redis.exists_returns_integer` will be removed in 5.0. You should use " \
16
+ "`exists?` instead."
17
+
18
+ ::Kernel.warn(message)
19
+ end
11
20
 
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)
21
+ @exists_returns_integer = value
22
+ end
23
+
24
+ attr_writer :current
17
25
  end
18
26
 
19
27
  def self.current
20
28
  @current ||= Redis.new
21
29
  end
22
30
 
23
- def self.current=(redis)
24
- @current = redis
25
- end
26
-
27
31
  include MonitorMixin
28
32
 
29
33
  # Create a new client instance
30
34
  #
31
35
  # @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.
36
+ # @option options [String] :url (value of the environment variable REDIS_URL) a Redis URL, for a TCP connection:
37
+ # `redis://:[password]@[hostname]:[port]/[db]` (password, port and database are optional), for a unix socket
38
+ # connection: `unix://[path to Redis socket]`. This overrides all other options.
33
39
  # @option options [String] :host ("127.0.0.1") server hostname
34
- # @option options [Fixnum] :port (6379) server port
40
+ # @option options [Integer] :port (6379) server port
35
41
  # @option options [String] :path path to server socket (overrides host and port)
36
42
  # @option options [Float] :timeout (5.0) timeout in seconds
37
43
  # @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds
44
+ # @option options [String] :username Username to authenticate against server
38
45
  # @option options [String] :password Password to authenticate against server
39
- # @option options [Fixnum] :db (0) Database to select after initial connect
46
+ # @option options [Integer] :db (0) Database to select after initial connect
40
47
  # @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
48
+ # @option options [String] :id ID for the client connection, assigns name to current connection by sending
49
+ # `CLIENT SETNAME`
50
+ # @option options [Hash, Integer] :tcp_keepalive Keepalive values, if Integer `intvl` and `probe` are calculated
51
+ # based on the value, if Hash `time`, `intvl` and `probes` can be specified as a Integer
52
+ # @option options [Integer] :reconnect_attempts Number of attempts trying to connect
44
53
  # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
45
54
  # @option options [Array] :sentinels List of sentinels to contact
46
55
  # @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave`
56
+ # @option options [Array<String, Hash{Symbol => String, Integer}>] :cluster List of cluster nodes to contact
57
+ # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not
58
+ # @option options [Class] :connector Class of custom connector
47
59
  #
48
60
  # @return [Redis] a new client instance
49
61
  def initialize(options = {})
50
62
  @options = options.dup
51
- @original_client = @client = Client.new(options)
63
+ @cluster_mode = options.key?(:cluster)
64
+ client = @cluster_mode ? Cluster : Client
65
+ @original_client = @client = client.new(options)
52
66
  @queue = Hash.new { |h, k| h[k] = [] }
53
67
 
54
68
  super() # Monitor#initialize
@@ -59,7 +73,7 @@ class Redis
59
73
  end
60
74
 
61
75
  # Run code with the client reconnecting
62
- def with_reconnect(val=true, &blk)
76
+ def with_reconnect(val = true, &blk)
63
77
  synchronize do |client|
64
78
  client.with_reconnect(val, &blk)
65
79
  end
@@ -102,7 +116,9 @@ class Redis
102
116
  # See http://redis.io/topics/pipelining for more details.
103
117
  #
104
118
  def queue(*command)
105
- @queue[Thread.current.object_id] << command
119
+ synchronize do
120
+ @queue[Thread.current.object_id] << command
121
+ end
106
122
  end
107
123
 
108
124
  # Sends all commands in the queue.
@@ -112,27 +128,37 @@ class Redis
112
128
  def commit
113
129
  synchronize do |client|
114
130
  begin
115
- client.call_pipelined(@queue[Thread.current.object_id])
131
+ pipeline = Pipeline.new(client)
132
+ @queue[Thread.current.object_id].each do |command|
133
+ pipeline.call(command)
134
+ end
135
+
136
+ client.call_pipelined(pipeline)
116
137
  ensure
117
138
  @queue.delete(Thread.current.object_id)
118
139
  end
119
140
  end
120
141
  end
121
142
 
143
+ def _client
144
+ @client
145
+ end
146
+
122
147
  # Authenticate to the server.
123
148
  #
124
- # @param [String] password must match the password specified in the
125
- # `requirepass` directive in the configuration file
149
+ # @param [Array<String>] args includes both username and password
150
+ # or only password
126
151
  # @return [String] `OK`
127
- def auth(password)
152
+ # @see https://redis.io/commands/auth AUTH command
153
+ def auth(*args)
128
154
  synchronize do |client|
129
- client.call([:auth, password])
155
+ client.call([:auth, *args])
130
156
  end
131
157
  end
132
158
 
133
159
  # Change the selected database for the current connection.
134
160
  #
135
- # @param [Fixnum] db zero-based index of the DB to use (0 to 15)
161
+ # @param [Integer] db zero-based index of the DB to use (0 to 15)
136
162
  # @return [String] `OK`
137
163
  def select(db)
138
164
  synchronize do |client|
@@ -143,10 +169,11 @@ class Redis
143
169
 
144
170
  # Ping the server.
145
171
  #
172
+ # @param [optional, String] message
146
173
  # @return [String] `PONG`
147
- def ping
174
+ def ping(message = nil)
148
175
  synchronize do |client|
149
- client.call([:ping])
176
+ client.call([:ping, message].compact)
150
177
  end
151
178
  end
152
179
 
@@ -200,7 +227,7 @@ class Redis
200
227
  def config(action, *args)
201
228
  synchronize do |client|
202
229
  client.call([:config, action] + args) do |reply|
203
- if reply.kind_of?(Array) && action == :get
230
+ if reply.is_a?(Array) && action == :get
204
231
  Hashify.call(reply)
205
232
  else
206
233
  reply
@@ -209,9 +236,28 @@ class Redis
209
236
  end
210
237
  end
211
238
 
239
+ # Manage client connections.
240
+ #
241
+ # @param [String, Symbol] subcommand e.g. `kill`, `list`, `getname`, `setname`
242
+ # @return [String, Hash] depends on subcommand
243
+ def client(subcommand = nil, *args)
244
+ synchronize do |client|
245
+ client.call([:client, subcommand] + args) do |reply|
246
+ if subcommand.to_s == "list"
247
+ reply.lines.map do |line|
248
+ entries = line.chomp.split(/[ =]/)
249
+ Hash[entries.each_slice(2).to_a]
250
+ end
251
+ else
252
+ reply
253
+ end
254
+ end
255
+ end
256
+ end
257
+
212
258
  # Return the number of keys in the selected database.
213
259
  #
214
- # @return [Fixnum]
260
+ # @return [Integer]
215
261
  def dbsize
216
262
  synchronize do |client|
217
263
  client.call([:dbsize])
@@ -226,19 +272,31 @@ class Redis
226
272
 
227
273
  # Remove all keys from all databases.
228
274
  #
275
+ # @param [Hash] options
276
+ # - `:async => Boolean`: async flush (default: false)
229
277
  # @return [String] `OK`
230
- def flushall
278
+ def flushall(options = nil)
231
279
  synchronize do |client|
232
- client.call([:flushall])
280
+ if options && options[:async]
281
+ client.call(%i[flushall async])
282
+ else
283
+ client.call([:flushall])
284
+ end
233
285
  end
234
286
  end
235
287
 
236
288
  # Remove all keys from the current database.
237
289
  #
290
+ # @param [Hash] options
291
+ # - `:async => Boolean`: async flush (default: false)
238
292
  # @return [String] `OK`
239
- def flushdb
293
+ def flushdb(options = nil)
240
294
  synchronize do |client|
241
- client.call([:flushdb])
295
+ if options && options[:async]
296
+ client.call(%i[flushdb async])
297
+ else
298
+ client.call([:flushdb])
299
+ end
242
300
  end
243
301
  end
244
302
 
@@ -249,10 +307,8 @@ class Redis
249
307
  def info(cmd = nil)
250
308
  synchronize do |client|
251
309
  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]
310
+ if reply.is_a?(String)
311
+ reply = HashifyInfo.call(reply)
256
312
 
257
313
  if cmd && cmd.to_s == "commandstats"
258
314
  # Extract nested hashes for INFO COMMANDSTATS
@@ -270,7 +326,7 @@ class Redis
270
326
 
271
327
  # Get the UNIX time stamp of the last successful save to disk.
272
328
  #
273
- # @return [Fixnum]
329
+ # @return [Integer]
274
330
  def lastsave
275
331
  synchronize do |client|
276
332
  client.call([:lastsave])
@@ -322,9 +378,9 @@ class Redis
322
378
  # Interact with the slowlog (get, len, reset)
323
379
  #
324
380
  # @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)
381
+ # @param [Integer] length maximum number of entries to return
382
+ # @return [Array<String>, Integer, String] depends on subcommand
383
+ def slowlog(subcommand, length = nil)
328
384
  synchronize do |client|
329
385
  args = [:slowlog, subcommand]
330
386
  args << length if length
@@ -344,12 +400,12 @@ class Redis
344
400
  # @example
345
401
  # r.time # => [ 1333093196, 606806 ]
346
402
  #
347
- # @return [Array<Fixnum>] tuple of seconds since UNIX epoch and
403
+ # @return [Array<Integer>] tuple of seconds since UNIX epoch and
348
404
  # microseconds in the current second
349
405
  def time
350
406
  synchronize do |client|
351
407
  client.call([:time]) do |reply|
352
- reply.map(&:to_i) if reply
408
+ reply&.map(&:to_i)
353
409
  end
354
410
  end
355
411
  end
@@ -367,7 +423,7 @@ class Redis
367
423
  # Set a key's time to live in seconds.
368
424
  #
369
425
  # @param [String] key
370
- # @param [Fixnum] seconds time to live
426
+ # @param [Integer] seconds time to live
371
427
  # @return [Boolean] whether the timeout was set or not
372
428
  def expire(key, seconds)
373
429
  synchronize do |client|
@@ -378,7 +434,7 @@ class Redis
378
434
  # Set the expiration for a key as a UNIX timestamp.
379
435
  #
380
436
  # @param [String] key
381
- # @param [Fixnum] unix_time expiry time specified as a UNIX timestamp
437
+ # @param [Integer] unix_time expiry time specified as a UNIX timestamp
382
438
  # @return [Boolean] whether the timeout was set or not
383
439
  def expireat(key, unix_time)
384
440
  synchronize do |client|
@@ -389,7 +445,7 @@ class Redis
389
445
  # Get the time to live (in seconds) for a key.
390
446
  #
391
447
  # @param [String] key
392
- # @return [Fixnum] remaining time to live in seconds.
448
+ # @return [Integer] remaining time to live in seconds.
393
449
  #
394
450
  # In Redis 2.6 or older the command returns -1 if the key does not exist or if
395
451
  # the key exist but has no associated expire.
@@ -407,7 +463,7 @@ class Redis
407
463
  # Set a key's time to live in milliseconds.
408
464
  #
409
465
  # @param [String] key
410
- # @param [Fixnum] milliseconds time to live
466
+ # @param [Integer] milliseconds time to live
411
467
  # @return [Boolean] whether the timeout was set or not
412
468
  def pexpire(key, milliseconds)
413
469
  synchronize do |client|
@@ -418,7 +474,7 @@ class Redis
418
474
  # Set the expiration for a key as number of milliseconds from UNIX Epoch.
419
475
  #
420
476
  # @param [String] key
421
- # @param [Fixnum] ms_unix_time expiry time specified as number of milliseconds from UNIX Epoch.
477
+ # @param [Integer] ms_unix_time expiry time specified as number of milliseconds from UNIX Epoch.
422
478
  # @return [Boolean] whether the timeout was set or not
423
479
  def pexpireat(key, ms_unix_time)
424
480
  synchronize do |client|
@@ -429,7 +485,7 @@ class Redis
429
485
  # Get the time to live (in milliseconds) for a key.
430
486
  #
431
487
  # @param [String] key
432
- # @return [Fixnum] remaining time to live in milliseconds
488
+ # @return [Integer] remaining time to live in milliseconds
433
489
  # In Redis 2.6 or older the command returns -1 if the key does not exist or if
434
490
  # the key exist but has no associated expire.
435
491
  #
@@ -458,50 +514,104 @@ class Redis
458
514
  # @param [String] key
459
515
  # @param [String] ttl
460
516
  # @param [String] serialized_value
517
+ # @param [Hash] options
518
+ # - `:replace => Boolean`: if false, raises an error if key already exists
519
+ # @raise [Redis::CommandError]
461
520
  # @return [String] `"OK"`
462
- def restore(key, ttl, serialized_value)
521
+ def restore(key, ttl, serialized_value, replace: nil)
522
+ args = [:restore, key, ttl, serialized_value]
523
+ args << 'REPLACE' if replace
524
+
463
525
  synchronize do |client|
464
- client.call([:restore, key, ttl, serialized_value])
526
+ client.call(args)
465
527
  end
466
528
  end
467
529
 
468
530
  # Transfer a key from the connected instance to another instance.
469
531
  #
470
- # @param [String] key
532
+ # @param [String, Array<String>] key
471
533
  # @param [Hash] options
472
534
  # - `:host => String`: host of instance to migrate to
473
535
  # - `:port => Integer`: port of instance to migrate to
474
536
  # - `:db => Integer`: database to migrate to (default: same as source)
475
537
  # - `:timeout => Integer`: timeout (default: same as connection timeout)
538
+ # - `:copy => Boolean`: Do not remove the key from the local instance.
539
+ # - `:replace => Boolean`: Replace existing key on the remote instance.
476
540
  # @return [String] `"OK"`
477
541
  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
542
+ args = [:migrate]
543
+ args << (options[:host] || raise(':host not specified'))
544
+ args << (options[:port] || raise(':port not specified'))
545
+ args << (key.is_a?(String) ? key : '')
546
+ args << (options[:db] || @client.db).to_i
547
+ args << (options[:timeout] || @client.timeout).to_i
548
+ args << 'COPY' if options[:copy]
549
+ args << 'REPLACE' if options[:replace]
550
+ args += ['KEYS', *key] if key.is_a?(Array)
482
551
 
483
- synchronize do |client|
484
- client.call([:migrate, host, port, key, db, timeout])
485
- end
552
+ synchronize { |client| client.call(args) }
486
553
  end
487
554
 
488
555
  # Delete one or more keys.
489
556
  #
490
557
  # @param [String, Array<String>] keys
491
- # @return [Fixnum] number of keys that were deleted
558
+ # @return [Integer] number of keys that were deleted
492
559
  def del(*keys)
560
+ keys.flatten!(1)
561
+ return 0 if keys.empty?
562
+
493
563
  synchronize do |client|
494
564
  client.call([:del] + keys)
495
565
  end
496
566
  end
497
567
 
498
- # Determine if a key exists.
568
+ # Unlink one or more keys.
499
569
  #
500
- # @param [String] key
570
+ # @param [String, Array<String>] keys
571
+ # @return [Integer] number of keys that were unlinked
572
+ def unlink(*keys)
573
+ synchronize do |client|
574
+ client.call([:unlink] + keys)
575
+ end
576
+ end
577
+
578
+ # Determine how many of the keys exists.
579
+ #
580
+ # @param [String, Array<String>] keys
581
+ # @return [Integer]
582
+ def exists(*keys)
583
+ if !Redis.exists_returns_integer && keys.size == 1
584
+ if Redis.exists_returns_integer.nil?
585
+ message = "`Redis#exists(key)` will return an Integer in redis-rb 4.3. `exists?` returns a boolean, you " \
586
+ "should use it instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = " \
587
+ "true. To disable this message and keep the current (boolean) behaviour of 'exists' you can set " \
588
+ "`Redis.exists_returns_integer = false`, but this option will be removed in 5.0. " \
589
+ "(#{::Kernel.caller(1, 1).first})\n"
590
+
591
+ ::Kernel.warn(message)
592
+ end
593
+
594
+ exists?(*keys)
595
+ else
596
+ _exists(*keys)
597
+ end
598
+ end
599
+
600
+ def _exists(*keys)
601
+ synchronize do |client|
602
+ client.call([:exists, *keys])
603
+ end
604
+ end
605
+
606
+ # Determine if any of the keys exists.
607
+ #
608
+ # @param [String, Array<String>] keys
501
609
  # @return [Boolean]
502
- def exists(key)
610
+ def exists?(*keys)
503
611
  synchronize do |client|
504
- client.call([:exists, key], &Boolify)
612
+ client.call([:exists, *keys]) do |value|
613
+ value > 0
614
+ end
505
615
  end
506
616
  end
507
617
 
@@ -512,7 +622,7 @@ class Redis
512
622
  def keys(pattern = "*")
513
623
  synchronize do |client|
514
624
  client.call([:keys, pattern]) do |reply|
515
- if reply.kind_of?(String)
625
+ if reply.is_a?(String)
516
626
  reply.split(" ")
517
627
  else
518
628
  reply
@@ -538,7 +648,7 @@ class Redis
538
648
  # # => "bar"
539
649
  #
540
650
  # @param [String] key
541
- # @param [Fixnum] db
651
+ # @param [Integer] db
542
652
  # @return [Boolean] whether the key was moved or not
543
653
  def move(key, db)
544
654
  synchronize do |client|
@@ -602,36 +712,33 @@ class Redis
602
712
  # - `:order => String`: combination of `ASC`, `DESC` and optionally `ALPHA`
603
713
  # - `:store => String`: key to store the result at
604
714
  #
605
- # @return [Array<String>, Array<Array<String>>, Fixnum]
715
+ # @return [Array<String>, Array<Array<String>>, Integer]
606
716
  # - when `:get` is not specified, or holds a single element, an array of elements
607
717
  # - when `:get` is specified, and holds more than one element, an array of
608
718
  # elements where every element is an array with the result for every
609
719
  # element specified in `:get`
610
720
  # - when `:store` is specified, the number of elements in the stored result
611
- def sort(key, options = {})
612
- args = []
721
+ def sort(key, by: nil, limit: nil, get: nil, order: nil, store: nil)
722
+ args = [:sort, key]
723
+ args << "BY" << by if by
613
724
 
614
- by = options[:by]
615
- args.concat(["BY", by]) if by
616
-
617
- limit = options[:limit]
618
- args.concat(["LIMIT"] + limit) if limit
725
+ if limit
726
+ args << "LIMIT"
727
+ args.concat(limit)
728
+ end
619
729
 
620
- get = Array(options[:get])
621
- args.concat(["GET"].product(get).flatten) unless get.empty?
730
+ get = Array(get)
731
+ get.each do |item|
732
+ args << "GET" << item
733
+ end
622
734
 
623
- order = options[:order]
624
735
  args.concat(order.split(" ")) if order
625
-
626
- store = options[:store]
627
- args.concat(["STORE", store]) if store
736
+ args << "STORE" << store if store
628
737
 
629
738
  synchronize do |client|
630
- client.call([:sort, key] + args) do |reply|
739
+ client.call(args) do |reply|
631
740
  if get.size > 1 && !store
632
- if reply
633
- reply.each_slice(get.size).to_a
634
- end
741
+ reply.each_slice(get.size).to_a if reply
635
742
  else
636
743
  reply
637
744
  end
@@ -656,7 +763,7 @@ class Redis
656
763
  # # => 4
657
764
  #
658
765
  # @param [String] key
659
- # @return [Fixnum] value after decrementing it
766
+ # @return [Integer] value after decrementing it
660
767
  def decr(key)
661
768
  synchronize do |client|
662
769
  client.call([:decr, key])
@@ -670,8 +777,8 @@ class Redis
670
777
  # # => 0
671
778
  #
672
779
  # @param [String] key
673
- # @param [Fixnum] decrement
674
- # @return [Fixnum] value after decrementing it
780
+ # @param [Integer] decrement
781
+ # @return [Integer] value after decrementing it
675
782
  def decrby(key, decrement)
676
783
  synchronize do |client|
677
784
  client.call([:decrby, key, decrement])
@@ -685,7 +792,7 @@ class Redis
685
792
  # # => 6
686
793
  #
687
794
  # @param [String] key
688
- # @return [Fixnum] value after incrementing it
795
+ # @return [Integer] value after incrementing it
689
796
  def incr(key)
690
797
  synchronize do |client|
691
798
  client.call([:incr, key])
@@ -699,8 +806,8 @@ class Redis
699
806
  # # => 10
700
807
  #
701
808
  # @param [String] key
702
- # @param [Fixnum] increment
703
- # @return [Fixnum] value after incrementing it
809
+ # @param [Integer] increment
810
+ # @return [Integer] value after incrementing it
704
811
  def incrby(key, increment)
705
812
  synchronize do |client|
706
813
  client.call([:incrby, key, increment])
@@ -727,41 +834,39 @@ class Redis
727
834
  # @param [String] key
728
835
  # @param [String] value
729
836
  # @param [Hash] options
730
- # - `:ex => Fixnum`: Set the specified expire time, in seconds.
731
- # - `:px => Fixnum`: Set the specified expire time, in milliseconds.
837
+ # - `:ex => Integer`: Set the specified expire time, in seconds.
838
+ # - `:px => Integer`: Set the specified expire time, in milliseconds.
839
+ # - `:exat => Integer` : Set the specified Unix time at which the key will expire, in seconds.
840
+ # - `:pxat => Integer` : Set the specified Unix time at which the key will expire, in milliseconds.
732
841
  # - `:nx => true`: Only set the key if it does not already exist.
733
842
  # - `:xx => true`: Only set the key if it already exist.
843
+ # - `:keepttl => true`: Retain the time to live associated with the key.
844
+ # - `:get => true`: Return the old string stored at key, or nil if key did not exist.
734
845
  # @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
846
+ def set(key, value, ex: nil, px: nil, exat: nil, pxat: nil, nx: nil, xx: nil, keepttl: nil, get: nil)
847
+ args = [:set, key, value.to_s]
848
+ args << "EX" << ex if ex
849
+ args << "PX" << px if px
850
+ args << "EXAT" << exat if exat
851
+ args << "PXAT" << pxat if pxat
852
+ args << "NX" if nx
853
+ args << "XX" if xx
854
+ args << "KEEPTTL" if keepttl
855
+ args << "GET" if get
749
856
 
750
857
  synchronize do |client|
751
858
  if nx || xx
752
- client.call([:set, key, value.to_s] + args, &BoolifySet)
859
+ client.call(args, &BoolifySet)
753
860
  else
754
- client.call([:set, key, value.to_s] + args)
861
+ client.call(args)
755
862
  end
756
863
  end
757
864
  end
758
865
 
759
- alias :[]= :set
760
-
761
866
  # Set the time to live in seconds of a key.
762
867
  #
763
868
  # @param [String] key
764
- # @param [Fixnum] ttl
869
+ # @param [Integer] ttl
765
870
  # @param [String] value
766
871
  # @return [String] `"OK"`
767
872
  def setex(key, ttl, value)
@@ -773,7 +878,7 @@ class Redis
773
878
  # Set the time to live in milliseconds of a key.
774
879
  #
775
880
  # @param [String] key
776
- # @param [Fixnum] ttl
881
+ # @param [Integer] ttl
777
882
  # @param [String] value
778
883
  # @return [String] `"OK"`
779
884
  def psetex(key, ttl, value)
@@ -835,7 +940,7 @@ class Redis
835
940
  # @see #mapped_msetnx
836
941
  def msetnx(*args)
837
942
  synchronize do |client|
838
- client.call([:msetnx] + args, &Boolify)
943
+ client.call([:msetnx, *args], &Boolify)
839
944
  end
840
945
  end
841
946
 
@@ -863,12 +968,10 @@ class Redis
863
968
  end
864
969
  end
865
970
 
866
- alias :[] :get
867
-
868
971
  # Get the values of all the given keys.
869
972
  #
870
973
  # @example
871
- # redis.mget("key1", "key1")
974
+ # redis.mget("key1", "key2")
872
975
  # # => ["v1", "v2"]
873
976
  #
874
977
  # @param [Array<String>] keys
@@ -877,7 +980,7 @@ class Redis
877
980
  # @see #mapped_mget
878
981
  def mget(*keys, &blk)
879
982
  synchronize do |client|
880
- client.call([:mget] + keys, &blk)
983
+ client.call([:mget, *keys], &blk)
881
984
  end
882
985
  end
883
986
 
@@ -893,7 +996,7 @@ class Redis
893
996
  # @see #mget
894
997
  def mapped_mget(*keys)
895
998
  mget(*keys) do |reply|
896
- if reply.kind_of?(Array)
999
+ if reply.is_a?(Array)
897
1000
  Hash[keys.zip(reply)]
898
1001
  else
899
1002
  reply
@@ -904,9 +1007,9 @@ class Redis
904
1007
  # Overwrite part of a string at key starting at the specified offset.
905
1008
  #
906
1009
  # @param [String] key
907
- # @param [Fixnum] offset byte offset
1010
+ # @param [Integer] offset byte offset
908
1011
  # @param [String] value
909
- # @return [Fixnum] length of the string after it was modified
1012
+ # @return [Integer] length of the string after it was modified
910
1013
  def setrange(key, offset, value)
911
1014
  synchronize do |client|
912
1015
  client.call([:setrange, key, offset, value.to_s])
@@ -916,10 +1019,10 @@ class Redis
916
1019
  # Get a substring of the string stored at a key.
917
1020
  #
918
1021
  # @param [String] key
919
- # @param [Fixnum] start zero-based start offset
920
- # @param [Fixnum] stop zero-based end offset. Use -1 for representing
1022
+ # @param [Integer] start zero-based start offset
1023
+ # @param [Integer] stop zero-based end offset. Use -1 for representing
921
1024
  # the end of the string
922
- # @return [Fixnum] `0` or `1`
1025
+ # @return [Integer] `0` or `1`
923
1026
  def getrange(key, start, stop)
924
1027
  synchronize do |client|
925
1028
  client.call([:getrange, key, start, stop])
@@ -929,9 +1032,9 @@ class Redis
929
1032
  # Sets or clears the bit at offset in the string value stored at key.
930
1033
  #
931
1034
  # @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`
1035
+ # @param [Integer] offset bit offset
1036
+ # @param [Integer] value bit value `0` or `1`
1037
+ # @return [Integer] the original bit value stored at `offset`
935
1038
  def setbit(key, offset, value)
936
1039
  synchronize do |client|
937
1040
  client.call([:setbit, key, offset, value])
@@ -941,8 +1044,8 @@ class Redis
941
1044
  # Returns the bit value at offset in the string value stored at key.
942
1045
  #
943
1046
  # @param [String] key
944
- # @param [Fixnum] offset bit offset
945
- # @return [Fixnum] `0` or `1`
1047
+ # @param [Integer] offset bit offset
1048
+ # @return [Integer] `0` or `1`
946
1049
  def getbit(key, offset)
947
1050
  synchronize do |client|
948
1051
  client.call([:getbit, key, offset])
@@ -953,7 +1056,7 @@ class Redis
953
1056
  #
954
1057
  # @param [String] key
955
1058
  # @param [String] value value to append
956
- # @return [Fixnum] length of the string after appending
1059
+ # @return [Integer] length of the string after appending
957
1060
  def append(key, value)
958
1061
  synchronize do |client|
959
1062
  client.call([:append, key, value])
@@ -963,9 +1066,9 @@ class Redis
963
1066
  # Count the number of set bits in a range of the string value stored at key.
964
1067
  #
965
1068
  # @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
1069
+ # @param [Integer] start start index
1070
+ # @param [Integer] stop stop index
1071
+ # @return [Integer] the number of bits set to 1
969
1072
  def bitcount(key, start = 0, stop = -1)
970
1073
  synchronize do |client|
971
1074
  client.call([:bitcount, key, start, stop])
@@ -977,25 +1080,23 @@ class Redis
977
1080
  # @param [String] operation e.g. `and`, `or`, `xor`, `not`
978
1081
  # @param [String] destkey destination key
979
1082
  # @param [String, Array<String>] keys one or more source keys to perform `operation`
980
- # @return [Fixnum] the length of the string stored in `destkey`
1083
+ # @return [Integer] the length of the string stored in `destkey`
981
1084
  def bitop(operation, destkey, *keys)
982
1085
  synchronize do |client|
983
- client.call([:bitop, operation, destkey] + keys)
1086
+ client.call([:bitop, operation, destkey, *keys])
984
1087
  end
985
1088
  end
986
1089
 
987
1090
  # Return the position of the first bit set to 1 or 0 in a string.
988
1091
  #
989
1092
  # @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.
1093
+ # @param [Integer] bit whether to look for the first 1 or 0 bit
1094
+ # @param [Integer] start start index
1095
+ # @param [Integer] stop stop index
1096
+ # @return [Integer] the position of the first 1/0 bit.
994
1097
  # -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
1098
+ def bitpos(key, bit, start = nil, stop = nil)
1099
+ raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
999
1100
 
1000
1101
  synchronize do |client|
1001
1102
  command = [:bitpos, key, bit]
@@ -1017,10 +1118,49 @@ class Redis
1017
1118
  end
1018
1119
  end
1019
1120
 
1121
+ # Get the value of key and delete the key. This command is similar to GET,
1122
+ # except for the fact that it also deletes the key on success.
1123
+ #
1124
+ # @param [String] key
1125
+ # @return [String] the old value stored in the key, or `nil` if the key
1126
+ # did not exist
1127
+ def getdel(key)
1128
+ synchronize do |client|
1129
+ client.call([:getdel, key])
1130
+ end
1131
+ end
1132
+
1133
+ # Get the value of key and optionally set its expiration. GETEX is similar to
1134
+ # GET, but is a write command with additional options. When no options are
1135
+ # provided, GETEX behaves like GET.
1136
+ #
1137
+ # @param [String] key
1138
+ # @param [Hash] options
1139
+ # - `:ex => Integer`: Set the specified expire time, in seconds.
1140
+ # - `:px => Integer`: Set the specified expire time, in milliseconds.
1141
+ # - `:exat => true`: Set the specified Unix time at which the key will
1142
+ # expire, in seconds.
1143
+ # - `:pxat => true`: Set the specified Unix time at which the key will
1144
+ # expire, in milliseconds.
1145
+ # - `:persist => true`: Remove the time to live associated with the key.
1146
+ # @return [String] The value of key, or nil when key does not exist.
1147
+ def getex(key, ex: nil, px: nil, exat: nil, pxat: nil, persist: false)
1148
+ args = [:getex, key]
1149
+ args << "EX" << ex if ex
1150
+ args << "PX" << px if px
1151
+ args << "EXAT" << exat if exat
1152
+ args << "PXAT" << pxat if pxat
1153
+ args << "PERSIST" if persist
1154
+
1155
+ synchronize do |client|
1156
+ client.call(args)
1157
+ end
1158
+ end
1159
+
1020
1160
  # Get the length of the value stored in a key.
1021
1161
  #
1022
1162
  # @param [String] key
1023
- # @return [Fixnum] the length of the value stored in the key, or 0
1163
+ # @return [Integer] the length of the value stored in the key, or 0
1024
1164
  # if the key does not exist
1025
1165
  def strlen(key)
1026
1166
  synchronize do |client|
@@ -1031,18 +1171,71 @@ class Redis
1031
1171
  # Get the length of a list.
1032
1172
  #
1033
1173
  # @param [String] key
1034
- # @return [Fixnum]
1174
+ # @return [Integer]
1035
1175
  def llen(key)
1036
1176
  synchronize do |client|
1037
1177
  client.call([:llen, key])
1038
1178
  end
1039
1179
  end
1040
1180
 
1181
+ # Remove the first/last element in a list, append/prepend it to another list and return it.
1182
+ #
1183
+ # @param [String] source source key
1184
+ # @param [String] destination destination key
1185
+ # @param [String, Symbol] where_source from where to remove the element from the source list
1186
+ # e.g. 'LEFT' - from head, 'RIGHT' - from tail
1187
+ # @param [String, Symbol] where_destination where to push the element to the source list
1188
+ # e.g. 'LEFT' - to head, 'RIGHT' - to tail
1189
+ #
1190
+ # @return [nil, String] the element, or nil when the source key does not exist
1191
+ #
1192
+ # @note This command comes in place of the now deprecated RPOPLPUSH.
1193
+ # Doing LMOVE RIGHT LEFT is equivalent.
1194
+ def lmove(source, destination, where_source, where_destination)
1195
+ where_source, where_destination = _normalize_move_wheres(where_source, where_destination)
1196
+
1197
+ synchronize do |client|
1198
+ client.call([:lmove, source, destination, where_source, where_destination])
1199
+ end
1200
+ end
1201
+
1202
+ # Remove the first/last element in a list and append/prepend it
1203
+ # to another list and return it, or block until one is available.
1204
+ #
1205
+ # @example With timeout
1206
+ # element = redis.blmove("foo", "bar", "LEFT", "RIGHT", timeout: 5)
1207
+ # # => nil on timeout
1208
+ # # => "element" on success
1209
+ # @example Without timeout
1210
+ # element = redis.blmove("foo", "bar", "LEFT", "RIGHT")
1211
+ # # => "element"
1212
+ #
1213
+ # @param [String] source source key
1214
+ # @param [String] destination destination key
1215
+ # @param [String, Symbol] where_source from where to remove the element from the source list
1216
+ # e.g. 'LEFT' - from head, 'RIGHT' - from tail
1217
+ # @param [String, Symbol] where_destination where to push the element to the source list
1218
+ # e.g. 'LEFT' - to head, 'RIGHT' - to tail
1219
+ # @param [Hash] options
1220
+ # - `:timeout => Numeric`: timeout in seconds, defaults to no timeout
1221
+ #
1222
+ # @return [nil, String] the element, or nil when the source key does not exist or the timeout expired
1223
+ #
1224
+ def blmove(source, destination, where_source, where_destination, timeout: 0)
1225
+ where_source, where_destination = _normalize_move_wheres(where_source, where_destination)
1226
+
1227
+ synchronize do |client|
1228
+ command = [:blmove, source, destination, where_source, where_destination, timeout]
1229
+ timeout += client.timeout if timeout > 0
1230
+ client.call_with_timeout(command, timeout)
1231
+ end
1232
+ end
1233
+
1041
1234
  # Prepend one or more values to a list, creating the list if it doesn't exist
1042
1235
  #
1043
1236
  # @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
1237
+ # @param [String, Array<String>] value string value, or array of string values to push
1238
+ # @return [Integer] the length of the list after the push operation
1046
1239
  def lpush(key, value)
1047
1240
  synchronize do |client|
1048
1241
  client.call([:lpush, key, value])
@@ -1053,7 +1246,7 @@ class Redis
1053
1246
  #
1054
1247
  # @param [String] key
1055
1248
  # @param [String] value
1056
- # @return [Fixnum] the length of the list after the push operation
1249
+ # @return [Integer] the length of the list after the push operation
1057
1250
  def lpushx(key, value)
1058
1251
  synchronize do |client|
1059
1252
  client.call([:lpushx, key, value])
@@ -1063,8 +1256,8 @@ class Redis
1063
1256
  # Append one or more values to a list, creating the list if it doesn't exist
1064
1257
  #
1065
1258
  # @param [String] key
1066
- # @param [String] value
1067
- # @return [Fixnum] the length of the list after the push operation
1259
+ # @param [String, Array<String>] value string value, or array of string values to push
1260
+ # @return [Integer] the length of the list after the push operation
1068
1261
  def rpush(key, value)
1069
1262
  synchronize do |client|
1070
1263
  client.call([:rpush, key, value])
@@ -1075,30 +1268,36 @@ class Redis
1075
1268
  #
1076
1269
  # @param [String] key
1077
1270
  # @param [String] value
1078
- # @return [Fixnum] the length of the list after the push operation
1271
+ # @return [Integer] the length of the list after the push operation
1079
1272
  def rpushx(key, value)
1080
1273
  synchronize do |client|
1081
1274
  client.call([:rpushx, key, value])
1082
1275
  end
1083
1276
  end
1084
1277
 
1085
- # Remove and get the first element in a list.
1278
+ # Remove and get the first elements in a list.
1086
1279
  #
1087
1280
  # @param [String] key
1088
- # @return [String]
1089
- def lpop(key)
1281
+ # @param [Integer] count number of elements to remove
1282
+ # @return [String, Array<String>] the values of the first elements
1283
+ def lpop(key, count = nil)
1090
1284
  synchronize do |client|
1091
- client.call([:lpop, key])
1285
+ command = [:lpop, key]
1286
+ command << count if count
1287
+ client.call(command)
1092
1288
  end
1093
1289
  end
1094
1290
 
1095
- # Remove and get the last element in a list.
1291
+ # Remove and get the last elements in a list.
1096
1292
  #
1097
1293
  # @param [String] key
1098
- # @return [String]
1099
- def rpop(key)
1294
+ # @param [Integer] count number of elements to remove
1295
+ # @return [String, Array<String>] the values of the last elements
1296
+ def rpop(key, count = nil)
1100
1297
  synchronize do |client|
1101
- client.call([:rpop, key])
1298
+ command = [:rpop, key]
1299
+ command << count if count
1300
+ client.call(command)
1102
1301
  end
1103
1302
  end
1104
1303
 
@@ -1113,28 +1312,27 @@ class Redis
1113
1312
  end
1114
1313
  end
1115
1314
 
1116
- def _bpop(cmd, args)
1117
- options = {}
1118
-
1119
- case args.last
1120
- when Hash
1315
+ def _bpop(cmd, args, &blk)
1316
+ timeout = if args.last.is_a?(Hash)
1121
1317
  options = args.pop
1122
- when Integer
1318
+ options[:timeout]
1319
+ elsif args.last.respond_to?(:to_int)
1123
1320
  # Issue deprecation notice in obnoxious mode...
1124
- options[:timeout] = args.pop
1321
+ args.pop.to_int
1125
1322
  end
1126
1323
 
1324
+ timeout ||= 0
1325
+
1127
1326
  if args.size > 1
1128
1327
  # Issue deprecation notice in obnoxious mode...
1129
1328
  end
1130
1329
 
1131
1330
  keys = args.flatten
1132
- timeout = options[:timeout] || 0
1133
1331
 
1134
1332
  synchronize do |client|
1135
1333
  command = [cmd, keys, timeout]
1136
1334
  timeout += client.timeout if timeout > 0
1137
- client.call_with_timeout(command, timeout)
1335
+ client.call_with_timeout(command, timeout, &blk)
1138
1336
  end
1139
1337
  end
1140
1338
 
@@ -1154,7 +1352,7 @@ class Redis
1154
1352
  # @param [String, Array<String>] keys one or more keys to perform the
1155
1353
  # blocking pop on
1156
1354
  # @param [Hash] options
1157
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1355
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
1158
1356
  #
1159
1357
  # @return [nil, [String, String]]
1160
1358
  # - `nil` when the operation timed out
@@ -1168,7 +1366,7 @@ class Redis
1168
1366
  # @param [String, Array<String>] keys one or more keys to perform the
1169
1367
  # blocking pop on
1170
1368
  # @param [Hash] options
1171
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1369
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
1172
1370
  #
1173
1371
  # @return [nil, [String, String]]
1174
1372
  # - `nil` when the operation timed out
@@ -1185,20 +1383,12 @@ class Redis
1185
1383
  # @param [String] source source key
1186
1384
  # @param [String] destination destination key
1187
1385
  # @param [Hash] options
1188
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1386
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
1189
1387
  #
1190
1388
  # @return [nil, String]
1191
1389
  # - `nil` when the operation timed out
1192
1390
  # - 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
-
1391
+ def brpoplpush(source, destination, deprecated_timeout = 0, timeout: deprecated_timeout)
1202
1392
  synchronize do |client|
1203
1393
  command = [:brpoplpush, source, destination, timeout]
1204
1394
  timeout += client.timeout if timeout > 0
@@ -1209,7 +1399,7 @@ class Redis
1209
1399
  # Get an element from a list by its index.
1210
1400
  #
1211
1401
  # @param [String] key
1212
- # @param [Fixnum] index
1402
+ # @param [Integer] index
1213
1403
  # @return [String]
1214
1404
  def lindex(key, index)
1215
1405
  synchronize do |client|
@@ -1223,7 +1413,7 @@ class Redis
1223
1413
  # @param [String, Symbol] where `BEFORE` or `AFTER`
1224
1414
  # @param [String] pivot reference element
1225
1415
  # @param [String] value
1226
- # @return [Fixnum] length of the list after the insert operation, or `-1`
1416
+ # @return [Integer] length of the list after the insert operation, or `-1`
1227
1417
  # when the element `pivot` was not found
1228
1418
  def linsert(key, where, pivot, value)
1229
1419
  synchronize do |client|
@@ -1234,8 +1424,8 @@ class Redis
1234
1424
  # Get a range of elements from a list.
1235
1425
  #
1236
1426
  # @param [String] key
1237
- # @param [Fixnum] start start index
1238
- # @param [Fixnum] stop stop index
1427
+ # @param [Integer] start start index
1428
+ # @param [Integer] stop stop index
1239
1429
  # @return [Array<String>]
1240
1430
  def lrange(key, start, stop)
1241
1431
  synchronize do |client|
@@ -1246,12 +1436,12 @@ class Redis
1246
1436
  # Remove elements from a list.
1247
1437
  #
1248
1438
  # @param [String] key
1249
- # @param [Fixnum] count number of elements to remove. Use a positive
1439
+ # @param [Integer] count number of elements to remove. Use a positive
1250
1440
  # value to remove the first `count` occurrences of `value`. A negative
1251
1441
  # value to remove the last `count` occurrences of `value`. Or zero, to
1252
1442
  # remove all occurrences of `value` from the list.
1253
1443
  # @param [String] value
1254
- # @return [Fixnum] the number of removed elements
1444
+ # @return [Integer] the number of removed elements
1255
1445
  def lrem(key, count, value)
1256
1446
  synchronize do |client|
1257
1447
  client.call([:lrem, key, count, value])
@@ -1261,7 +1451,7 @@ class Redis
1261
1451
  # Set the value of an element in a list by its index.
1262
1452
  #
1263
1453
  # @param [String] key
1264
- # @param [Fixnum] index
1454
+ # @param [Integer] index
1265
1455
  # @param [String] value
1266
1456
  # @return [String] `OK`
1267
1457
  def lset(key, index, value)
@@ -1273,8 +1463,8 @@ class Redis
1273
1463
  # Trim a list to the specified range.
1274
1464
  #
1275
1465
  # @param [String] key
1276
- # @param [Fixnum] start start index
1277
- # @param [Fixnum] stop stop index
1466
+ # @param [Integer] start start index
1467
+ # @param [Integer] stop stop index
1278
1468
  # @return [String] `OK`
1279
1469
  def ltrim(key, start, stop)
1280
1470
  synchronize do |client|
@@ -1285,7 +1475,7 @@ class Redis
1285
1475
  # Get the number of members in a set.
1286
1476
  #
1287
1477
  # @param [String] key
1288
- # @return [Fixnum]
1478
+ # @return [Integer]
1289
1479
  def scard(key)
1290
1480
  synchronize do |client|
1291
1481
  client.call([:scard, key])
@@ -1296,8 +1486,8 @@ class Redis
1296
1486
  #
1297
1487
  # @param [String] key
1298
1488
  # @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
1489
+ # @return [Boolean, Integer] `Boolean` when a single member is specified,
1490
+ # holding whether or not adding the member succeeded, or `Integer` when an
1301
1491
  # array of members is specified, holding the number of members that were
1302
1492
  # successfully added
1303
1493
  def sadd(key, member)
@@ -1318,8 +1508,8 @@ class Redis
1318
1508
  #
1319
1509
  # @param [String] key
1320
1510
  # @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
1511
+ # @return [Boolean, Integer] `Boolean` when a single member is specified,
1512
+ # holding whether or not removing the member succeeded, or `Integer` when an
1323
1513
  # array of members is specified, holding the number of members that were
1324
1514
  # successfully removed
1325
1515
  def srem(key, member)
@@ -1340,7 +1530,7 @@ class Redis
1340
1530
  #
1341
1531
  # @param [String] key
1342
1532
  # @return [String]
1343
- # @param [Fixnum] count
1533
+ # @param [Integer] count
1344
1534
  def spop(key, count = nil)
1345
1535
  synchronize do |client|
1346
1536
  if count.nil?
@@ -1354,7 +1544,7 @@ class Redis
1354
1544
  # Get one or more random members from a set.
1355
1545
  #
1356
1546
  # @param [String] key
1357
- # @param [Fixnum] count
1547
+ # @param [Integer] count
1358
1548
  # @return [String]
1359
1549
  def srandmember(key, count = nil)
1360
1550
  synchronize do |client|
@@ -1389,6 +1579,19 @@ class Redis
1389
1579
  end
1390
1580
  end
1391
1581
 
1582
+ # Determine if multiple values are members of a set.
1583
+ #
1584
+ # @param [String] key
1585
+ # @param [String, Array<String>] members
1586
+ # @return [Array<Boolean>]
1587
+ def smismember(key, *members)
1588
+ synchronize do |client|
1589
+ client.call([:smismember, key, *members]) do |reply|
1590
+ reply.map(&Boolify)
1591
+ end
1592
+ end
1593
+ end
1594
+
1392
1595
  # Get all the members in a set.
1393
1596
  #
1394
1597
  # @param [String] key
@@ -1405,7 +1608,7 @@ class Redis
1405
1608
  # @return [Array<String>] members in the difference
1406
1609
  def sdiff(*keys)
1407
1610
  synchronize do |client|
1408
- client.call([:sdiff] + keys)
1611
+ client.call([:sdiff, *keys])
1409
1612
  end
1410
1613
  end
1411
1614
 
@@ -1413,10 +1616,10 @@ class Redis
1413
1616
  #
1414
1617
  # @param [String] destination destination key
1415
1618
  # @param [String, Array<String>] keys keys pointing to sets to subtract
1416
- # @return [Fixnum] number of elements in the resulting set
1619
+ # @return [Integer] number of elements in the resulting set
1417
1620
  def sdiffstore(destination, *keys)
1418
1621
  synchronize do |client|
1419
- client.call([:sdiffstore, destination] + keys)
1622
+ client.call([:sdiffstore, destination, *keys])
1420
1623
  end
1421
1624
  end
1422
1625
 
@@ -1426,7 +1629,7 @@ class Redis
1426
1629
  # @return [Array<String>] members in the intersection
1427
1630
  def sinter(*keys)
1428
1631
  synchronize do |client|
1429
- client.call([:sinter] + keys)
1632
+ client.call([:sinter, *keys])
1430
1633
  end
1431
1634
  end
1432
1635
 
@@ -1434,10 +1637,10 @@ class Redis
1434
1637
  #
1435
1638
  # @param [String] destination destination key
1436
1639
  # @param [String, Array<String>] keys keys pointing to sets to intersect
1437
- # @return [Fixnum] number of elements in the resulting set
1640
+ # @return [Integer] number of elements in the resulting set
1438
1641
  def sinterstore(destination, *keys)
1439
1642
  synchronize do |client|
1440
- client.call([:sinterstore, destination] + keys)
1643
+ client.call([:sinterstore, destination, *keys])
1441
1644
  end
1442
1645
  end
1443
1646
 
@@ -1447,7 +1650,7 @@ class Redis
1447
1650
  # @return [Array<String>] members in the union
1448
1651
  def sunion(*keys)
1449
1652
  synchronize do |client|
1450
- client.call([:sunion] + keys)
1653
+ client.call([:sunion, *keys])
1451
1654
  end
1452
1655
  end
1453
1656
 
@@ -1455,10 +1658,10 @@ class Redis
1455
1658
  #
1456
1659
  # @param [String] destination destination key
1457
1660
  # @param [String, Array<String>] keys keys pointing to sets to unify
1458
- # @return [Fixnum] number of elements in the resulting set
1661
+ # @return [Integer] number of elements in the resulting set
1459
1662
  def sunionstore(destination, *keys)
1460
1663
  synchronize do |client|
1461
- client.call([:sunionstore, destination] + keys)
1664
+ client.call([:sunionstore, destination, *keys])
1462
1665
  end
1463
1666
  end
1464
1667
 
@@ -1469,7 +1672,7 @@ class Redis
1469
1672
  # # => 4
1470
1673
  #
1471
1674
  # @param [String] key
1472
- # @return [Fixnum]
1675
+ # @return [Integer]
1473
1676
  def zcard(key)
1474
1677
  synchronize do |client|
1475
1678
  client.call([:zcard, key])
@@ -1493,6 +1696,10 @@ class Redis
1493
1696
  # add elements)
1494
1697
  # - `:nx => true`: Don't update already existing elements (always
1495
1698
  # add new elements)
1699
+ # - `:lt => true`: Only update existing elements if the new score
1700
+ # is less than the current score
1701
+ # - `:gt => true`: Only update existing elements if the new score
1702
+ # is greater than the current score
1496
1703
  # - `:ch => true`: Modify the return value from the number of new
1497
1704
  # elements added, to the total number of elements changed (CH is an
1498
1705
  # abbreviation of changed); changed elements are new elements added
@@ -1500,38 +1707,29 @@ class Redis
1500
1707
  # - `:incr => true`: When this option is specified ZADD acts like
1501
1708
  # ZINCRBY; only one score-element pair can be specified in this mode
1502
1709
  #
1503
- # @return [Boolean, Fixnum, Float]
1710
+ # @return [Boolean, Integer, Float]
1504
1711
  # - `Boolean` when a single pair is specified, holding whether or not it was
1505
1712
  # **added** to the sorted set.
1506
- # - `Fixnum` when an array of pairs is specified, holding the number of
1713
+ # - `Integer` when an array of pairs is specified, holding the number of
1507
1714
  # pairs that were **added** to the sorted set.
1508
1715
  # - `Float` when option :incr is specified, holding the score of the member
1509
1716
  # 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
1717
+ def zadd(key, *args, nx: nil, xx: nil, lt: nil, gt: nil, ch: nil, incr: nil)
1718
+ command = [:zadd, key]
1719
+ command << "NX" if nx
1720
+ command << "XX" if xx
1721
+ command << "LT" if lt
1722
+ command << "GT" if gt
1723
+ command << "CH" if ch
1724
+ command << "INCR" if incr
1527
1725
 
1528
1726
  synchronize do |client|
1529
1727
  if args.size == 1 && args[0].is_a?(Array)
1530
1728
  # Variadic: return float if INCR, integer if !INCR
1531
- client.call([:zadd, key] + zadd_options + args[0], &(incr ? Floatify : nil))
1729
+ client.call(command + args[0], &(incr ? Floatify : nil))
1532
1730
  elsif args.size == 2
1533
1731
  # Single pair: return float if INCR, boolean if !INCR
1534
- client.call([:zadd, key] + zadd_options + args, &(incr ? Floatify : Boolify))
1732
+ client.call(command + args, &(incr ? Floatify : Boolify))
1535
1733
  else
1536
1734
  raise ArgumentError, "wrong number of arguments"
1537
1735
  end
@@ -1566,10 +1764,10 @@ class Redis
1566
1764
  # - a single member
1567
1765
  # - an array of members
1568
1766
  #
1569
- # @return [Boolean, Fixnum]
1767
+ # @return [Boolean, Integer]
1570
1768
  # - `Boolean` when a single member is specified, holding whether or not it
1571
1769
  # was removed from the sorted set
1572
- # - `Fixnum` when an array of pairs is specified, holding the number of
1770
+ # - `Integer` when an array of pairs is specified, holding the number of
1573
1771
  # members that were removed to the sorted set
1574
1772
  def zrem(key, member)
1575
1773
  synchronize do |client|
@@ -1585,6 +1783,90 @@ class Redis
1585
1783
  end
1586
1784
  end
1587
1785
 
1786
+ # Removes and returns up to count members with the highest scores in the sorted set stored at key.
1787
+ #
1788
+ # @example Popping a member
1789
+ # redis.zpopmax('zset')
1790
+ # #=> ['b', 2.0]
1791
+ # @example With count option
1792
+ # redis.zpopmax('zset', 2)
1793
+ # #=> [['b', 2.0], ['a', 1.0]]
1794
+ #
1795
+ # @params key [String] a key of the sorted set
1796
+ # @params count [Integer] a number of members
1797
+ #
1798
+ # @return [Array<String, Float>] element and score pair if count is not specified
1799
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1800
+ def zpopmax(key, count = nil)
1801
+ synchronize do |client|
1802
+ members = client.call([:zpopmax, key, count].compact, &FloatifyPairs)
1803
+ count.to_i > 1 ? members : members.first
1804
+ end
1805
+ end
1806
+
1807
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at key.
1808
+ #
1809
+ # @example Popping a member
1810
+ # redis.zpopmin('zset')
1811
+ # #=> ['a', 1.0]
1812
+ # @example With count option
1813
+ # redis.zpopmin('zset', 2)
1814
+ # #=> [['a', 1.0], ['b', 2.0]]
1815
+ #
1816
+ # @params key [String] a key of the sorted set
1817
+ # @params count [Integer] a number of members
1818
+ #
1819
+ # @return [Array<String, Float>] element and score pair if count is not specified
1820
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1821
+ def zpopmin(key, count = nil)
1822
+ synchronize do |client|
1823
+ members = client.call([:zpopmin, key, count].compact, &FloatifyPairs)
1824
+ count.to_i > 1 ? members : members.first
1825
+ end
1826
+ end
1827
+
1828
+ # Removes and returns up to count members with the highest scores in the sorted set stored at keys,
1829
+ # or block until one is available.
1830
+ #
1831
+ # @example Popping a member from a sorted set
1832
+ # redis.bzpopmax('zset', 1)
1833
+ # #=> ['zset', 'b', 2.0]
1834
+ # @example Popping a member from multiple sorted sets
1835
+ # redis.bzpopmax('zset1', 'zset2', 1)
1836
+ # #=> ['zset1', 'b', 2.0]
1837
+ #
1838
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1839
+ # @params timeout [Integer] the maximum number of seconds to block
1840
+ #
1841
+ # @return [Array<String, String, Float>] a touple of key, member and score
1842
+ # @return [nil] when no element could be popped and the timeout expired
1843
+ def bzpopmax(*args)
1844
+ _bpop(:bzpopmax, args) do |reply|
1845
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1846
+ end
1847
+ end
1848
+
1849
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at keys,
1850
+ # or block until one is available.
1851
+ #
1852
+ # @example Popping a member from a sorted set
1853
+ # redis.bzpopmin('zset', 1)
1854
+ # #=> ['zset', 'a', 1.0]
1855
+ # @example Popping a member from multiple sorted sets
1856
+ # redis.bzpopmin('zset1', 'zset2', 1)
1857
+ # #=> ['zset1', 'a', 1.0]
1858
+ #
1859
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1860
+ # @params timeout [Integer] the maximum number of seconds to block
1861
+ #
1862
+ # @return [Array<String, String, Float>] a touple of key, member and score
1863
+ # @return [nil] when no element could be popped and the timeout expired
1864
+ def bzpopmin(*args)
1865
+ _bpop(:bzpopmin, args) do |reply|
1866
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1867
+ end
1868
+ end
1869
+
1588
1870
  # Get the score associated with the given member in a sorted set.
1589
1871
  #
1590
1872
  # @example Get the score for member "a"
@@ -1600,6 +1882,63 @@ class Redis
1600
1882
  end
1601
1883
  end
1602
1884
 
1885
+ # Get the scores associated with the given members in a sorted set.
1886
+ #
1887
+ # @example Get the scores for members "a" and "b"
1888
+ # redis.zmscore("zset", "a", "b")
1889
+ # # => [32.0, 48.0]
1890
+ #
1891
+ # @param [String] key
1892
+ # @param [String, Array<String>] members
1893
+ # @return [Array<Float>] scores of the members
1894
+ def zmscore(key, *members)
1895
+ synchronize do |client|
1896
+ client.call([:zmscore, key, *members]) do |reply|
1897
+ reply.map(&Floatify)
1898
+ end
1899
+ end
1900
+ end
1901
+
1902
+ # Get one or more random members from a sorted set.
1903
+ #
1904
+ # @example Get one random member
1905
+ # redis.zrandmember("zset")
1906
+ # # => "a"
1907
+ # @example Get multiple random members
1908
+ # redis.zrandmember("zset", 2)
1909
+ # # => ["a", "b"]
1910
+ # @example Gem multiple random members with scores
1911
+ # redis.zrandmember("zset", 2, with_scores: true)
1912
+ # # => [["a", 2.0], ["b", 3.0]]
1913
+ #
1914
+ # @param [String] key
1915
+ # @param [Integer] count
1916
+ # @param [Hash] options
1917
+ # - `:with_scores => true`: include scores in output
1918
+ #
1919
+ # @return [nil, String, Array<String>, Array<[String, Float]>]
1920
+ # - when `key` does not exist or set is empty, `nil`
1921
+ # - when `count` is not specified, a member
1922
+ # - when `count` is specified and `:with_scores` is not specified, an array of members
1923
+ # - when `:with_scores` is specified, an array with `[member, score]` pairs
1924
+ def zrandmember(key, count = nil, withscores: false, with_scores: withscores)
1925
+ if with_scores && count.nil?
1926
+ raise ArgumentError, "count argument must be specified"
1927
+ end
1928
+
1929
+ args = [:zrandmember, key]
1930
+ args << count if count
1931
+
1932
+ if with_scores
1933
+ args << "WITHSCORES"
1934
+ block = FloatifyPairs
1935
+ end
1936
+
1937
+ synchronize do |client|
1938
+ client.call(args, &block)
1939
+ end
1940
+ end
1941
+
1603
1942
  # Return a range of members in a sorted set, by index.
1604
1943
  #
1605
1944
  # @example Retrieve all members from a sorted set
@@ -1610,18 +1949,16 @@ class Redis
1610
1949
  # # => [["a", 32.0], ["b", 64.0]]
1611
1950
  #
1612
1951
  # @param [String] key
1613
- # @param [Fixnum] start start index
1614
- # @param [Fixnum] stop stop index
1952
+ # @param [Integer] start start index
1953
+ # @param [Integer] stop stop index
1615
1954
  # @param [Hash] options
1616
1955
  # - `:with_scores => true`: include scores in output
1617
1956
  #
1618
1957
  # @return [Array<String>, Array<[String, Float]>]
1619
1958
  # - when `:with_scores` is not specified, an array of members
1620
1959
  # - 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]
1960
+ def zrange(key, start, stop, withscores: false, with_scores: withscores)
1961
+ args = [:zrange, key, start, stop]
1625
1962
 
1626
1963
  if with_scores
1627
1964
  args << "WITHSCORES"
@@ -1629,7 +1966,7 @@ class Redis
1629
1966
  end
1630
1967
 
1631
1968
  synchronize do |client|
1632
- client.call([:zrange, key, start, stop] + args, &block)
1969
+ client.call(args, &block)
1633
1970
  end
1634
1971
  end
1635
1972
 
@@ -1644,10 +1981,8 @@ class Redis
1644
1981
  # # => [["b", 64.0], ["a", 32.0]]
1645
1982
  #
1646
1983
  # @see #zrange
1647
- def zrevrange(key, start, stop, options = {})
1648
- args = []
1649
-
1650
- with_scores = options[:with_scores] || options[:withscores]
1984
+ def zrevrange(key, start, stop, withscores: false, with_scores: withscores)
1985
+ args = [:zrevrange, key, start, stop]
1651
1986
 
1652
1987
  if with_scores
1653
1988
  args << "WITHSCORES"
@@ -1655,7 +1990,7 @@ class Redis
1655
1990
  end
1656
1991
 
1657
1992
  synchronize do |client|
1658
- client.call([:zrevrange, key, start, stop] + args, &block)
1993
+ client.call(args, &block)
1659
1994
  end
1660
1995
  end
1661
1996
 
@@ -1663,7 +1998,7 @@ class Redis
1663
1998
  #
1664
1999
  # @param [String] key
1665
2000
  # @param [String] member
1666
- # @return [Fixnum]
2001
+ # @return [Integer]
1667
2002
  def zrank(key, member)
1668
2003
  synchronize do |client|
1669
2004
  client.call([:zrank, key, member])
@@ -1675,7 +2010,7 @@ class Redis
1675
2010
  #
1676
2011
  # @param [String] key
1677
2012
  # @param [String] member
1678
- # @return [Fixnum]
2013
+ # @return [Integer]
1679
2014
  def zrevrank(key, member)
1680
2015
  synchronize do |client|
1681
2016
  client.call([:zrevrank, key, member])
@@ -1692,15 +2027,39 @@ class Redis
1692
2027
  # # => 5
1693
2028
  #
1694
2029
  # @param [String] key
1695
- # @param [Fixnum] start start index
1696
- # @param [Fixnum] stop stop index
1697
- # @return [Fixnum] number of members that were removed
2030
+ # @param [Integer] start start index
2031
+ # @param [Integer] stop stop index
2032
+ # @return [Integer] number of members that were removed
1698
2033
  def zremrangebyrank(key, start, stop)
1699
2034
  synchronize do |client|
1700
2035
  client.call([:zremrangebyrank, key, start, stop])
1701
2036
  end
1702
2037
  end
1703
2038
 
2039
+ # Count the members, with the same score in a sorted set, within the given lexicographical range.
2040
+ #
2041
+ # @example Count members matching a
2042
+ # redis.zlexcount("zset", "[a", "[a\xff")
2043
+ # # => 1
2044
+ # @example Count members matching a-z
2045
+ # redis.zlexcount("zset", "[a", "[z\xff")
2046
+ # # => 26
2047
+ #
2048
+ # @param [String] key
2049
+ # @param [String] min
2050
+ # - inclusive minimum is specified by prefixing `(`
2051
+ # - exclusive minimum is specified by prefixing `[`
2052
+ # @param [String] max
2053
+ # - inclusive maximum is specified by prefixing `(`
2054
+ # - exclusive maximum is specified by prefixing `[`
2055
+ #
2056
+ # @return [Integer] number of members within the specified lexicographical range
2057
+ def zlexcount(key, min, max)
2058
+ synchronize do |client|
2059
+ client.call([:zlexcount, key, min, max])
2060
+ end
2061
+ end
2062
+
1704
2063
  # Return a range of members with the same score in a sorted set, by lexicographical ordering
1705
2064
  #
1706
2065
  # @example Retrieve members matching a
@@ -1722,14 +2081,16 @@ class Redis
1722
2081
  # `count` members
1723
2082
  #
1724
2083
  # @return [Array<String>, Array<[String, Float]>]
1725
- def zrangebylex(key, min, max, options = {})
1726
- args = []
2084
+ def zrangebylex(key, min, max, limit: nil)
2085
+ args = [:zrangebylex, key, min, max]
1727
2086
 
1728
- limit = options[:limit]
1729
- args.concat(["LIMIT"] + limit) if limit
2087
+ if limit
2088
+ args << "LIMIT"
2089
+ args.concat(limit)
2090
+ end
1730
2091
 
1731
2092
  synchronize do |client|
1732
- client.call([:zrangebylex, key, min, max] + args)
2093
+ client.call(args)
1733
2094
  end
1734
2095
  end
1735
2096
 
@@ -1744,14 +2105,16 @@ class Redis
1744
2105
  # # => ["abbygail", "abby"]
1745
2106
  #
1746
2107
  # @see #zrangebylex
1747
- def zrevrangebylex(key, max, min, options = {})
1748
- args = []
2108
+ def zrevrangebylex(key, max, min, limit: nil)
2109
+ args = [:zrevrangebylex, key, max, min]
1749
2110
 
1750
- limit = options[:limit]
1751
- args.concat(["LIMIT"] + limit) if limit
2111
+ if limit
2112
+ args << "LIMIT"
2113
+ args.concat(limit)
2114
+ end
1752
2115
 
1753
2116
  synchronize do |client|
1754
- client.call([:zrevrangebylex, key, max, min] + args)
2117
+ client.call(args)
1755
2118
  end
1756
2119
  end
1757
2120
 
@@ -1782,21 +2145,21 @@ class Redis
1782
2145
  # @return [Array<String>, Array<[String, Float]>]
1783
2146
  # - when `:with_scores` is not specified, an array of members
1784
2147
  # - 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]
2148
+ def zrangebyscore(key, min, max, withscores: false, with_scores: withscores, limit: nil)
2149
+ args = [:zrangebyscore, key, min, max]
1789
2150
 
1790
2151
  if with_scores
1791
2152
  args << "WITHSCORES"
1792
2153
  block = FloatifyPairs
1793
2154
  end
1794
2155
 
1795
- limit = options[:limit]
1796
- args.concat(["LIMIT"] + limit) if limit
2156
+ if limit
2157
+ args << "LIMIT"
2158
+ args.concat(limit)
2159
+ end
1797
2160
 
1798
2161
  synchronize do |client|
1799
- client.call([:zrangebyscore, key, min, max] + args, &block)
2162
+ client.call(args, &block)
1800
2163
  end
1801
2164
  end
1802
2165
 
@@ -1814,21 +2177,21 @@ class Redis
1814
2177
  # # => [["b", 64.0], ["a", 32.0]]
1815
2178
  #
1816
2179
  # @see #zrangebyscore
1817
- def zrevrangebyscore(key, max, min, options = {})
1818
- args = []
1819
-
1820
- with_scores = options[:with_scores] || options[:withscores]
2180
+ def zrevrangebyscore(key, max, min, withscores: false, with_scores: withscores, limit: nil)
2181
+ args = [:zrevrangebyscore, key, max, min]
1821
2182
 
1822
2183
  if with_scores
1823
- args << ["WITHSCORES"]
2184
+ args << "WITHSCORES"
1824
2185
  block = FloatifyPairs
1825
2186
  end
1826
2187
 
1827
- limit = options[:limit]
1828
- args.concat(["LIMIT"] + limit) if limit
2188
+ if limit
2189
+ args << "LIMIT"
2190
+ args.concat(limit)
2191
+ end
1829
2192
 
1830
2193
  synchronize do |client|
1831
- client.call([:zrevrangebyscore, key, max, min] + args, &block)
2194
+ client.call(args, &block)
1832
2195
  end
1833
2196
  end
1834
2197
 
@@ -1848,7 +2211,7 @@ class Redis
1848
2211
  # @param [String] max
1849
2212
  # - inclusive maximum score is specified verbatim
1850
2213
  # - exclusive maximum score is specified by prefixing `(`
1851
- # @return [Fixnum] number of members that were removed
2214
+ # @return [Integer] number of members that were removed
1852
2215
  def zremrangebyscore(key, min, max)
1853
2216
  synchronize do |client|
1854
2217
  client.call([:zremrangebyscore, key, min, max])
@@ -1871,13 +2234,52 @@ class Redis
1871
2234
  # @param [String] max
1872
2235
  # - inclusive maximum score is specified verbatim
1873
2236
  # - exclusive maximum score is specified by prefixing `(`
1874
- # @return [Fixnum] number of members in within the specified range
2237
+ # @return [Integer] number of members in within the specified range
1875
2238
  def zcount(key, min, max)
1876
2239
  synchronize do |client|
1877
2240
  client.call([:zcount, key, min, max])
1878
2241
  end
1879
2242
  end
1880
2243
 
2244
+ # Return the intersection of multiple sorted sets
2245
+ #
2246
+ # @example Retrieve the intersection of `2*zsetA` and `1*zsetB`
2247
+ # redis.zinter("zsetA", "zsetB", :weights => [2.0, 1.0])
2248
+ # # => ["v1", "v2"]
2249
+ # @example Retrieve the intersection of `2*zsetA` and `1*zsetB`, and their scores
2250
+ # redis.zinter("zsetA", "zsetB", :weights => [2.0, 1.0], :with_scores => true)
2251
+ # # => [["v1", 3.0], ["v2", 6.0]]
2252
+ #
2253
+ # @param [String, Array<String>] keys one or more keys to intersect
2254
+ # @param [Hash] options
2255
+ # - `:weights => [Float, Float, ...]`: weights to associate with source
2256
+ # sorted sets
2257
+ # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
2258
+ # - `:with_scores => true`: include scores in output
2259
+ #
2260
+ # @return [Array<String>, Array<[String, Float]>]
2261
+ # - when `:with_scores` is not specified, an array of members
2262
+ # - when `:with_scores` is specified, an array with `[member, score]` pairs
2263
+ def zinter(*keys, weights: nil, aggregate: nil, with_scores: false)
2264
+ args = [:zinter, keys.size, *keys]
2265
+
2266
+ if weights
2267
+ args << "WEIGHTS"
2268
+ args.concat(weights)
2269
+ end
2270
+
2271
+ args << "AGGREGATE" << aggregate if aggregate
2272
+
2273
+ if with_scores
2274
+ args << "WITHSCORES"
2275
+ block = FloatifyPairs
2276
+ end
2277
+
2278
+ synchronize do |client|
2279
+ client.call(args, &block)
2280
+ end
2281
+ end
2282
+
1881
2283
  # Intersect multiple sorted sets and store the resulting sorted set in a new
1882
2284
  # key.
1883
2285
  #
@@ -1891,18 +2293,19 @@ class Redis
1891
2293
  # - `:weights => [Float, Float, ...]`: weights to associate with source
1892
2294
  # sorted sets
1893
2295
  # - `: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 = []
2296
+ # @return [Integer] number of elements in the resulting sorted set
2297
+ def zinterstore(destination, keys, weights: nil, aggregate: nil)
2298
+ args = [:zinterstore, destination, keys.size, *keys]
1897
2299
 
1898
- weights = options[:weights]
1899
- args.concat(["WEIGHTS"] + weights) if weights
2300
+ if weights
2301
+ args << "WEIGHTS"
2302
+ args.concat(weights)
2303
+ end
1900
2304
 
1901
- aggregate = options[:aggregate]
1902
- args.concat(["AGGREGATE", aggregate]) if aggregate
2305
+ args << "AGGREGATE" << aggregate if aggregate
1903
2306
 
1904
2307
  synchronize do |client|
1905
- client.call([:zinterstore, destination, keys.size] + keys + args)
2308
+ client.call(args)
1906
2309
  end
1907
2310
  end
1908
2311
 
@@ -1918,40 +2321,46 @@ class Redis
1918
2321
  # - `:weights => [Float, Float, ...]`: weights to associate with source
1919
2322
  # sorted sets
1920
2323
  # - `: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 = []
2324
+ # @return [Integer] number of elements in the resulting sorted set
2325
+ def zunionstore(destination, keys, weights: nil, aggregate: nil)
2326
+ args = [:zunionstore, destination, keys.size, *keys]
1924
2327
 
1925
- weights = options[:weights]
1926
- args.concat(["WEIGHTS"] + weights) if weights
2328
+ if weights
2329
+ args << "WEIGHTS"
2330
+ args.concat(weights)
2331
+ end
1927
2332
 
1928
- aggregate = options[:aggregate]
1929
- args.concat(["AGGREGATE", aggregate]) if aggregate
2333
+ args << "AGGREGATE" << aggregate if aggregate
1930
2334
 
1931
2335
  synchronize do |client|
1932
- client.call([:zunionstore, destination, keys.size] + keys + args)
2336
+ client.call(args)
1933
2337
  end
1934
2338
  end
1935
2339
 
1936
2340
  # Get the number of fields in a hash.
1937
2341
  #
1938
2342
  # @param [String] key
1939
- # @return [Fixnum] number of fields in the hash
2343
+ # @return [Integer] number of fields in the hash
1940
2344
  def hlen(key)
1941
2345
  synchronize do |client|
1942
2346
  client.call([:hlen, key])
1943
2347
  end
1944
2348
  end
1945
2349
 
1946
- # Set the string value of a hash field.
2350
+ # Set one or more hash values.
2351
+ #
2352
+ # @example
2353
+ # redis.hset("hash", "f1", "v1", "f2", "v2") # => 2
2354
+ # redis.hset("hash", { "f1" => "v1", "f2" => "v2" }) # => 2
1947
2355
  #
1948
2356
  # @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)
2357
+ # @param [Array<String> | Hash<String, String>] attrs array or hash of fields and values
2358
+ # @return [Integer] The number of fields that were added to the hash
2359
+ def hset(key, *attrs)
2360
+ attrs = attrs.first.flatten if attrs.size == 1 && attrs.first.is_a?(Hash)
2361
+
1953
2362
  synchronize do |client|
1954
- client.call([:hset, key, field, value], &Boolify)
2363
+ client.call([:hset, key, *attrs])
1955
2364
  end
1956
2365
  end
1957
2366
 
@@ -2040,7 +2449,7 @@ class Redis
2040
2449
  # @see #hmget
2041
2450
  def mapped_hmget(key, *fields)
2042
2451
  hmget(key, *fields) do |reply|
2043
- if reply.kind_of?(Array)
2452
+ if reply.is_a?(Array)
2044
2453
  Hash[fields.zip(reply)]
2045
2454
  else
2046
2455
  reply
@@ -2052,10 +2461,10 @@ class Redis
2052
2461
  #
2053
2462
  # @param [String] key
2054
2463
  # @param [String, Array<String>] field
2055
- # @return [Fixnum] the number of fields that were removed from the hash
2056
- def hdel(key, field)
2464
+ # @return [Integer] the number of fields that were removed from the hash
2465
+ def hdel(key, *fields)
2057
2466
  synchronize do |client|
2058
- client.call([:hdel, key, field])
2467
+ client.call([:hdel, key, *fields])
2059
2468
  end
2060
2469
  end
2061
2470
 
@@ -2074,8 +2483,8 @@ class Redis
2074
2483
  #
2075
2484
  # @param [String] key
2076
2485
  # @param [String] field
2077
- # @param [Fixnum] increment
2078
- # @return [Fixnum] value of the field after incrementing it
2486
+ # @param [Integer] increment
2487
+ # @return [Integer] value of the field after incrementing it
2079
2488
  def hincrby(key, field, increment)
2080
2489
  synchronize do |client|
2081
2490
  client.call([:hincrby, key, field, increment])
@@ -2133,20 +2542,21 @@ class Redis
2133
2542
 
2134
2543
  def subscribed?
2135
2544
  synchronize do |client|
2136
- client.kind_of? SubscribedClient
2545
+ client.is_a? SubscribedClient
2137
2546
  end
2138
2547
  end
2139
2548
 
2140
2549
  # Listen for messages published to the given channels.
2141
2550
  def subscribe(*channels, &block)
2142
- synchronize do |client|
2551
+ synchronize do |_client|
2143
2552
  _subscription(:subscribe, 0, channels, block)
2144
2553
  end
2145
2554
  end
2146
2555
 
2147
- # Listen for messages published to the given channels. Throw a timeout error if there is no messages for a timeout period.
2556
+ # Listen for messages published to the given channels. Throw a timeout error
2557
+ # if there is no messages for a timeout period.
2148
2558
  def subscribe_with_timeout(timeout, *channels, &block)
2149
- synchronize do |client|
2559
+ synchronize do |_client|
2150
2560
  _subscription(:subscribe_with_timeout, timeout, channels, block)
2151
2561
  end
2152
2562
  end
@@ -2154,21 +2564,23 @@ class Redis
2154
2564
  # Stop listening for messages posted to the given channels.
2155
2565
  def unsubscribe(*channels)
2156
2566
  synchronize do |client|
2157
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2567
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2568
+
2158
2569
  client.unsubscribe(*channels)
2159
2570
  end
2160
2571
  end
2161
2572
 
2162
2573
  # Listen for messages published to channels matching the given patterns.
2163
2574
  def psubscribe(*channels, &block)
2164
- synchronize do |client|
2575
+ synchronize do |_client|
2165
2576
  _subscription(:psubscribe, 0, channels, block)
2166
2577
  end
2167
2578
  end
2168
2579
 
2169
- # Listen for messages published to channels matching the given patterns. Throw a timeout error if there is no messages for a timeout period.
2580
+ # Listen for messages published to channels matching the given patterns.
2581
+ # Throw a timeout error if there is no messages for a timeout period.
2170
2582
  def psubscribe_with_timeout(timeout, *channels, &block)
2171
- synchronize do |client|
2583
+ synchronize do |_client|
2172
2584
  _subscription(:psubscribe_with_timeout, timeout, channels, block)
2173
2585
  end
2174
2586
  end
@@ -2176,7 +2588,8 @@ class Redis
2176
2588
  # Stop listening for messages posted to channels matching the given patterns.
2177
2589
  def punsubscribe(*channels)
2178
2590
  synchronize do |client|
2179
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2591
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2592
+
2180
2593
  client.punsubscribe(*channels)
2181
2594
  end
2182
2595
  end
@@ -2221,7 +2634,7 @@ class Redis
2221
2634
  # @see #multi
2222
2635
  def watch(*keys)
2223
2636
  synchronize do |client|
2224
- res = client.call([:watch] + keys)
2637
+ res = client.call([:watch, *keys])
2225
2638
 
2226
2639
  if block_given?
2227
2640
  begin
@@ -2251,13 +2664,13 @@ class Redis
2251
2664
  end
2252
2665
 
2253
2666
  def pipelined
2254
- synchronize do |client|
2667
+ synchronize do |prior_client|
2255
2668
  begin
2256
- original, @client = @client, Pipeline.new
2669
+ @client = Pipeline.new(prior_client)
2257
2670
  yield(self)
2258
- original.call_pipeline(@client)
2671
+ prior_client.call_pipeline(@client)
2259
2672
  ensure
2260
- @client = original
2673
+ @client = prior_client
2261
2674
  end
2262
2675
  end
2263
2676
  end
@@ -2293,17 +2706,16 @@ class Redis
2293
2706
  # @see #watch
2294
2707
  # @see #unwatch
2295
2708
  def multi
2296
- synchronize do |client|
2709
+ synchronize do |prior_client|
2297
2710
  if !block_given?
2298
- client.call([:multi])
2711
+ prior_client.call([:multi])
2299
2712
  else
2300
2713
  begin
2301
- pipeline = Pipeline::Multi.new
2302
- original, @client = @client, pipeline
2714
+ @client = Pipeline::Multi.new(prior_client)
2303
2715
  yield(self)
2304
- original.call_pipeline(pipeline)
2716
+ prior_client.call_pipeline(@client)
2305
2717
  ensure
2306
- @client = original
2718
+ @client = prior_client
2307
2719
  end
2308
2720
  end
2309
2721
  end
@@ -2450,18 +2862,13 @@ class Redis
2450
2862
  _eval(:evalsha, args)
2451
2863
  end
2452
2864
 
2453
- def _scan(command, cursor, args, options = {}, &block)
2865
+ def _scan(command, cursor, args, match: nil, count: nil, type: nil, &block)
2454
2866
  # SSCAN/ZSCAN/HSCAN already prepend the key to +args+.
2455
2867
 
2456
2868
  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
2869
+ args << "MATCH" << match if match
2870
+ args << "COUNT" << count if count
2871
+ args << "TYPE" << type if type
2465
2872
 
2466
2873
  synchronize do |client|
2467
2874
  client.call([command] + args, &block)
@@ -2476,15 +2883,19 @@ class Redis
2476
2883
  # @example Retrieve a batch of keys matching a pattern
2477
2884
  # redis.scan(4, :match => "key:1?")
2478
2885
  # # => ["92", ["key:13", "key:18"]]
2886
+ # @example Retrieve a batch of keys of a certain type
2887
+ # redis.scan(92, :type => "zset")
2888
+ # # => ["173", ["sortedset:14", "sortedset:78"]]
2479
2889
  #
2480
2890
  # @param [String, Integer] cursor the cursor of the iteration
2481
2891
  # @param [Hash] options
2482
2892
  # - `:match => String`: only return keys matching the pattern
2483
2893
  # - `:count => Integer`: return count keys at most per iteration
2894
+ # - `:type => String`: return keys only of the given type
2484
2895
  #
2485
2896
  # @return [String, Array<String>] the next cursor and all found keys
2486
- def scan(cursor, options={})
2487
- _scan(:scan, cursor, [], options)
2897
+ def scan(cursor, **options)
2898
+ _scan(:scan, cursor, [], **options)
2488
2899
  end
2489
2900
 
2490
2901
  # Scan the keyspace
@@ -2496,17 +2907,23 @@ class Redis
2496
2907
  # redis.scan_each(:match => "key:1?") {|key| puts key}
2497
2908
  # # => key:13
2498
2909
  # # => key:18
2910
+ # @example Execute block for each key of a type
2911
+ # redis.scan_each(:type => "hash") {|key| puts redis.type(key)}
2912
+ # # => "hash"
2913
+ # # => "hash"
2499
2914
  #
2500
2915
  # @param [Hash] options
2501
2916
  # - `:match => String`: only return keys matching the pattern
2502
2917
  # - `:count => Integer`: return count keys at most per iteration
2918
+ # - `:type => String`: return keys only of the given type
2503
2919
  #
2504
2920
  # @return [Enumerator] an enumerator for all found keys
2505
- def scan_each(options={}, &block)
2506
- return to_enum(:scan_each, options) unless block_given?
2921
+ def scan_each(**options, &block)
2922
+ return to_enum(:scan_each, **options) unless block_given?
2923
+
2507
2924
  cursor = 0
2508
2925
  loop do
2509
- cursor, keys = scan(cursor, options)
2926
+ cursor, keys = scan(cursor, **options)
2510
2927
  keys.each(&block)
2511
2928
  break if cursor == "0"
2512
2929
  end
@@ -2523,8 +2940,8 @@ class Redis
2523
2940
  # - `:count => Integer`: return count keys at most per iteration
2524
2941
  #
2525
2942
  # @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|
2943
+ def hscan(key, cursor, **options)
2944
+ _scan(:hscan, cursor, [key], **options) do |reply|
2528
2945
  [reply[0], reply[1].each_slice(2).to_a]
2529
2946
  end
2530
2947
  end
@@ -2540,11 +2957,12 @@ class Redis
2540
2957
  # - `:count => Integer`: return count keys at most per iteration
2541
2958
  #
2542
2959
  # @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?
2960
+ def hscan_each(key, **options, &block)
2961
+ return to_enum(:hscan_each, key, **options) unless block_given?
2962
+
2545
2963
  cursor = 0
2546
2964
  loop do
2547
- cursor, values = hscan(key, cursor, options)
2965
+ cursor, values = hscan(key, cursor, **options)
2548
2966
  values.each(&block)
2549
2967
  break if cursor == "0"
2550
2968
  end
@@ -2562,8 +2980,8 @@ class Redis
2562
2980
  #
2563
2981
  # @return [String, Array<[String, Float]>] the next cursor and all found
2564
2982
  # members and scores
2565
- def zscan(key, cursor, options={})
2566
- _scan(:zscan, cursor, [key], options) do |reply|
2983
+ def zscan(key, cursor, **options)
2984
+ _scan(:zscan, cursor, [key], **options) do |reply|
2567
2985
  [reply[0], FloatifyPairs.call(reply[1])]
2568
2986
  end
2569
2987
  end
@@ -2579,11 +2997,12 @@ class Redis
2579
2997
  # - `:count => Integer`: return count keys at most per iteration
2580
2998
  #
2581
2999
  # @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?
3000
+ def zscan_each(key, **options, &block)
3001
+ return to_enum(:zscan_each, key, **options) unless block_given?
3002
+
2584
3003
  cursor = 0
2585
3004
  loop do
2586
- cursor, values = zscan(key, cursor, options)
3005
+ cursor, values = zscan(key, cursor, **options)
2587
3006
  values.each(&block)
2588
3007
  break if cursor == "0"
2589
3008
  end
@@ -2600,8 +3019,8 @@ class Redis
2600
3019
  # - `:count => Integer`: return count keys at most per iteration
2601
3020
  #
2602
3021
  # @return [String, Array<String>] the next cursor and all found members
2603
- def sscan(key, cursor, options={})
2604
- _scan(:sscan, cursor, [key], options)
3022
+ def sscan(key, cursor, **options)
3023
+ _scan(:sscan, cursor, [key], **options)
2605
3024
  end
2606
3025
 
2607
3026
  # Scan a set
@@ -2615,11 +3034,12 @@ class Redis
2615
3034
  # - `:count => Integer`: return count keys at most per iteration
2616
3035
  #
2617
3036
  # @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?
3037
+ def sscan_each(key, **options, &block)
3038
+ return to_enum(:sscan_each, key, **options) unless block_given?
3039
+
2620
3040
  cursor = 0
2621
3041
  loop do
2622
- cursor, keys = sscan(key, cursor, options)
3042
+ cursor, keys = sscan(key, cursor, **options)
2623
3043
  keys.each(&block)
2624
3044
  break if cursor == "0"
2625
3045
  end
@@ -2642,7 +3062,7 @@ class Redis
2642
3062
  # union of the HyperLogLogs contained in the keys.
2643
3063
  #
2644
3064
  # @param [String, Array<String>] keys
2645
- # @return [Fixnum]
3065
+ # @return [Integer]
2646
3066
  def pfcount(*keys)
2647
3067
  synchronize do |client|
2648
3068
  client.call([:pfcount] + keys)
@@ -2661,6 +3081,445 @@ class Redis
2661
3081
  end
2662
3082
  end
2663
3083
 
3084
+ # Adds the specified geospatial items (latitude, longitude, name) to the specified key
3085
+ #
3086
+ # @param [String] key
3087
+ # @param [Array] member arguemnts for member or members: longitude, latitude, name
3088
+ # @return [Integer] number of elements added to the sorted set
3089
+ def geoadd(key, *member)
3090
+ synchronize do |client|
3091
+ client.call([:geoadd, key, *member])
3092
+ end
3093
+ end
3094
+
3095
+ # Returns geohash string representing position for specified members of the specified key.
3096
+ #
3097
+ # @param [String] key
3098
+ # @param [String, Array<String>] member one member or array of members
3099
+ # @return [Array<String, nil>] returns array containg geohash string if member is present, nil otherwise
3100
+ def geohash(key, member)
3101
+ synchronize do |client|
3102
+ client.call([:geohash, key, member])
3103
+ end
3104
+ end
3105
+
3106
+ # Query a sorted set representing a geospatial index to fetch members matching a
3107
+ # given maximum distance from a point
3108
+ #
3109
+ # @param [Array] args key, longitude, latitude, radius, unit(m|km|ft|mi)
3110
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest
3111
+ # or the farthest to the nearest relative to the center
3112
+ # @param [Integer] count limit the results to the first N matching items
3113
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
3114
+ # @return [Array<String>] may be changed with `options`
3115
+
3116
+ def georadius(*args, **geoptions)
3117
+ geoarguments = _geoarguments(*args, **geoptions)
3118
+
3119
+ synchronize do |client|
3120
+ client.call([:georadius, *geoarguments])
3121
+ end
3122
+ end
3123
+
3124
+ # Query a sorted set representing a geospatial index to fetch members matching a
3125
+ # given maximum distance from an already existing member
3126
+ #
3127
+ # @param [Array] args key, member, radius, unit(m|km|ft|mi)
3128
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest
3129
+ # to the nearest relative to the center
3130
+ # @param [Integer] count limit the results to the first N matching items
3131
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
3132
+ # @return [Array<String>] may be changed with `options`
3133
+
3134
+ def georadiusbymember(*args, **geoptions)
3135
+ geoarguments = _geoarguments(*args, **geoptions)
3136
+
3137
+ synchronize do |client|
3138
+ client.call([:georadiusbymember, *geoarguments])
3139
+ end
3140
+ end
3141
+
3142
+ # Returns longitude and latitude of members of a geospatial index
3143
+ #
3144
+ # @param [String] key
3145
+ # @param [String, Array<String>] member one member or array of members
3146
+ # @return [Array<Array<String>, nil>] returns array of elements, where each
3147
+ # element is either array of longitude and latitude or nil
3148
+ def geopos(key, member)
3149
+ synchronize do |client|
3150
+ client.call([:geopos, key, member])
3151
+ end
3152
+ end
3153
+
3154
+ # Returns the distance between two members of a geospatial index
3155
+ #
3156
+ # @param [String ]key
3157
+ # @param [Array<String>] members
3158
+ # @param ['m', 'km', 'mi', 'ft'] unit
3159
+ # @return [String, nil] returns distance in spefied unit if both members present, nil otherwise.
3160
+ def geodist(key, member1, member2, unit = 'm')
3161
+ synchronize do |client|
3162
+ client.call([:geodist, key, member1, member2, unit])
3163
+ end
3164
+ end
3165
+
3166
+ # Returns the stream information each subcommand.
3167
+ #
3168
+ # @example stream
3169
+ # redis.xinfo(:stream, 'mystream')
3170
+ # @example groups
3171
+ # redis.xinfo(:groups, 'mystream')
3172
+ # @example consumers
3173
+ # redis.xinfo(:consumers, 'mystream', 'mygroup')
3174
+ #
3175
+ # @param subcommand [String] e.g. `stream` `groups` `consumers`
3176
+ # @param key [String] the stream key
3177
+ # @param group [String] the consumer group name, required if subcommand is `consumers`
3178
+ #
3179
+ # @return [Hash] information of the stream if subcommand is `stream`
3180
+ # @return [Array<Hash>] information of the consumer groups if subcommand is `groups`
3181
+ # @return [Array<Hash>] information of the consumers if subcommand is `consumers`
3182
+ def xinfo(subcommand, key, group = nil)
3183
+ args = [:xinfo, subcommand, key, group].compact
3184
+ synchronize do |client|
3185
+ client.call(args) do |reply|
3186
+ case subcommand.to_s.downcase
3187
+ when 'stream' then Hashify.call(reply)
3188
+ when 'groups', 'consumers' then reply.map { |arr| Hashify.call(arr) }
3189
+ else reply
3190
+ end
3191
+ end
3192
+ end
3193
+ end
3194
+
3195
+ # Add new entry to the stream.
3196
+ #
3197
+ # @example Without options
3198
+ # redis.xadd('mystream', f1: 'v1', f2: 'v2')
3199
+ # @example With options
3200
+ # redis.xadd('mystream', { f1: 'v1', f2: 'v2' }, id: '0-0', maxlen: 1000, approximate: true)
3201
+ #
3202
+ # @param key [String] the stream key
3203
+ # @param entry [Hash] one or multiple field-value pairs
3204
+ # @param opts [Hash] several options for `XADD` command
3205
+ #
3206
+ # @option opts [String] :id the entry id, default value is `*`, it means auto generation
3207
+ # @option opts [Integer] :maxlen max length of entries
3208
+ # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen or not
3209
+ #
3210
+ # @return [String] the entry id
3211
+ def xadd(key, entry, approximate: nil, maxlen: nil, id: '*')
3212
+ args = [:xadd, key]
3213
+ if maxlen
3214
+ args << "MAXLEN"
3215
+ args << "~" if approximate
3216
+ args << maxlen
3217
+ end
3218
+ args << id
3219
+ args.concat(entry.to_a.flatten)
3220
+ synchronize { |client| client.call(args) }
3221
+ end
3222
+
3223
+ # Trims older entries of the stream if needed.
3224
+ #
3225
+ # @example Without options
3226
+ # redis.xtrim('mystream', 1000)
3227
+ # @example With options
3228
+ # redis.xtrim('mystream', 1000, approximate: true)
3229
+ #
3230
+ # @param key [String] the stream key
3231
+ # @param mexlen [Integer] max length of entries
3232
+ # @param approximate [Boolean] whether to add `~` modifier of maxlen or not
3233
+ #
3234
+ # @return [Integer] the number of entries actually deleted
3235
+ def xtrim(key, maxlen, approximate: false)
3236
+ args = [:xtrim, key, 'MAXLEN', (approximate ? '~' : nil), maxlen].compact
3237
+ synchronize { |client| client.call(args) }
3238
+ end
3239
+
3240
+ # Delete entries by entry ids.
3241
+ #
3242
+ # @example With splatted entry ids
3243
+ # redis.xdel('mystream', '0-1', '0-2')
3244
+ # @example With arrayed entry ids
3245
+ # redis.xdel('mystream', ['0-1', '0-2'])
3246
+ #
3247
+ # @param key [String] the stream key
3248
+ # @param ids [Array<String>] one or multiple entry ids
3249
+ #
3250
+ # @return [Integer] the number of entries actually deleted
3251
+ def xdel(key, *ids)
3252
+ args = [:xdel, key].concat(ids.flatten)
3253
+ synchronize { |client| client.call(args) }
3254
+ end
3255
+
3256
+ # Fetches entries of the stream in ascending order.
3257
+ #
3258
+ # @example Without options
3259
+ # redis.xrange('mystream')
3260
+ # @example With a specific start
3261
+ # redis.xrange('mystream', '0-1')
3262
+ # @example With a specific start and end
3263
+ # redis.xrange('mystream', '0-1', '0-3')
3264
+ # @example With count options
3265
+ # redis.xrange('mystream', count: 10)
3266
+ #
3267
+ # @param key [String] the stream key
3268
+ # @param start [String] first entry id of range, default value is `-`
3269
+ # @param end [String] last entry id of range, default value is `+`
3270
+ # @param count [Integer] the number of entries as limit
3271
+ #
3272
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3273
+ def xrange(key, start = '-', range_end = '+', count: nil)
3274
+ args = [:xrange, key, start, range_end]
3275
+ args.concat(['COUNT', count]) if count
3276
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3277
+ end
3278
+
3279
+ # Fetches entries of the stream in descending order.
3280
+ #
3281
+ # @example Without options
3282
+ # redis.xrevrange('mystream')
3283
+ # @example With a specific end
3284
+ # redis.xrevrange('mystream', '0-3')
3285
+ # @example With a specific end and start
3286
+ # redis.xrevrange('mystream', '0-3', '0-1')
3287
+ # @example With count options
3288
+ # redis.xrevrange('mystream', count: 10)
3289
+ #
3290
+ # @param key [String] the stream key
3291
+ # @param end [String] first entry id of range, default value is `+`
3292
+ # @param start [String] last entry id of range, default value is `-`
3293
+ # @params count [Integer] the number of entries as limit
3294
+ #
3295
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3296
+ def xrevrange(key, range_end = '+', start = '-', count: nil)
3297
+ args = [:xrevrange, key, range_end, start]
3298
+ args.concat(['COUNT', count]) if count
3299
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3300
+ end
3301
+
3302
+ # Returns the number of entries inside a stream.
3303
+ #
3304
+ # @example With key
3305
+ # redis.xlen('mystream')
3306
+ #
3307
+ # @param key [String] the stream key
3308
+ #
3309
+ # @return [Integer] the number of entries
3310
+ def xlen(key)
3311
+ synchronize { |client| client.call([:xlen, key]) }
3312
+ end
3313
+
3314
+ # Fetches entries from one or multiple streams. Optionally blocking.
3315
+ #
3316
+ # @example With a key
3317
+ # redis.xread('mystream', '0-0')
3318
+ # @example With multiple keys
3319
+ # redis.xread(%w[mystream1 mystream2], %w[0-0 0-0])
3320
+ # @example With count option
3321
+ # redis.xread('mystream', '0-0', count: 2)
3322
+ # @example With block option
3323
+ # redis.xread('mystream', '$', block: 1000)
3324
+ #
3325
+ # @param keys [Array<String>] one or multiple stream keys
3326
+ # @param ids [Array<String>] one or multiple entry ids
3327
+ # @param count [Integer] the number of entries as limit per stream
3328
+ # @param block [Integer] the number of milliseconds as blocking timeout
3329
+ #
3330
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3331
+ def xread(keys, ids, count: nil, block: nil)
3332
+ args = [:xread]
3333
+ args << 'COUNT' << count if count
3334
+ args << 'BLOCK' << block.to_i if block
3335
+ _xread(args, keys, ids, block)
3336
+ end
3337
+
3338
+ # Manages the consumer group of the stream.
3339
+ #
3340
+ # @example With `create` subcommand
3341
+ # redis.xgroup(:create, 'mystream', 'mygroup', '$')
3342
+ # @example With `setid` subcommand
3343
+ # redis.xgroup(:setid, 'mystream', 'mygroup', '$')
3344
+ # @example With `destroy` subcommand
3345
+ # redis.xgroup(:destroy, 'mystream', 'mygroup')
3346
+ # @example With `delconsumer` subcommand
3347
+ # redis.xgroup(:delconsumer, 'mystream', 'mygroup', 'consumer1')
3348
+ #
3349
+ # @param subcommand [String] `create` `setid` `destroy` `delconsumer`
3350
+ # @param key [String] the stream key
3351
+ # @param group [String] the consumer group name
3352
+ # @param id_or_consumer [String]
3353
+ # * the entry id or `$`, required if subcommand is `create` or `setid`
3354
+ # * the consumer name, required if subcommand is `delconsumer`
3355
+ # @param mkstream [Boolean] whether to create an empty stream automatically or not
3356
+ #
3357
+ # @return [String] `OK` if subcommand is `create` or `setid`
3358
+ # @return [Integer] effected count if subcommand is `destroy` or `delconsumer`
3359
+ def xgroup(subcommand, key, group, id_or_consumer = nil, mkstream: false)
3360
+ args = [:xgroup, subcommand, key, group, id_or_consumer, (mkstream ? 'MKSTREAM' : nil)].compact
3361
+ synchronize { |client| client.call(args) }
3362
+ end
3363
+
3364
+ # Fetches a subset of the entries from one or multiple streams related with the consumer group.
3365
+ # Optionally blocking.
3366
+ #
3367
+ # @example With a key
3368
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>')
3369
+ # @example With multiple keys
3370
+ # redis.xreadgroup('mygroup', 'consumer1', %w[mystream1 mystream2], %w[> >])
3371
+ # @example With count option
3372
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', count: 2)
3373
+ # @example With block option
3374
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', block: 1000)
3375
+ # @example With noack option
3376
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', noack: true)
3377
+ #
3378
+ # @param group [String] the consumer group name
3379
+ # @param consumer [String] the consumer name
3380
+ # @param keys [Array<String>] one or multiple stream keys
3381
+ # @param ids [Array<String>] one or multiple entry ids
3382
+ # @param opts [Hash] several options for `XREADGROUP` command
3383
+ #
3384
+ # @option opts [Integer] :count the number of entries as limit
3385
+ # @option opts [Integer] :block the number of milliseconds as blocking timeout
3386
+ # @option opts [Boolean] :noack whether message loss is acceptable or not
3387
+ #
3388
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3389
+ def xreadgroup(group, consumer, keys, ids, count: nil, block: nil, noack: nil)
3390
+ args = [:xreadgroup, 'GROUP', group, consumer]
3391
+ args << 'COUNT' << count if count
3392
+ args << 'BLOCK' << block.to_i if block
3393
+ args << 'NOACK' if noack
3394
+ _xread(args, keys, ids, block)
3395
+ end
3396
+
3397
+ # Removes one or multiple entries from the pending entries list of a stream consumer group.
3398
+ #
3399
+ # @example With a entry id
3400
+ # redis.xack('mystream', 'mygroup', '1526569495631-0')
3401
+ # @example With splatted entry ids
3402
+ # redis.xack('mystream', 'mygroup', '0-1', '0-2')
3403
+ # @example With arrayed entry ids
3404
+ # redis.xack('mystream', 'mygroup', %w[0-1 0-2])
3405
+ #
3406
+ # @param key [String] the stream key
3407
+ # @param group [String] the consumer group name
3408
+ # @param ids [Array<String>] one or multiple entry ids
3409
+ #
3410
+ # @return [Integer] the number of entries successfully acknowledged
3411
+ def xack(key, group, *ids)
3412
+ args = [:xack, key, group].concat(ids.flatten)
3413
+ synchronize { |client| client.call(args) }
3414
+ end
3415
+
3416
+ # Changes the ownership of a pending entry
3417
+ #
3418
+ # @example With splatted entry ids
3419
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-1', '0-2')
3420
+ # @example With arrayed entry ids
3421
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2])
3422
+ # @example With idle option
3423
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], idle: 1000)
3424
+ # @example With time option
3425
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], time: 1542866959000)
3426
+ # @example With retrycount option
3427
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], retrycount: 10)
3428
+ # @example With force option
3429
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], force: true)
3430
+ # @example With justid option
3431
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], justid: true)
3432
+ #
3433
+ # @param key [String] the stream key
3434
+ # @param group [String] the consumer group name
3435
+ # @param consumer [String] the consumer name
3436
+ # @param min_idle_time [Integer] the number of milliseconds
3437
+ # @param ids [Array<String>] one or multiple entry ids
3438
+ # @param opts [Hash] several options for `XCLAIM` command
3439
+ #
3440
+ # @option opts [Integer] :idle the number of milliseconds as last time it was delivered of the entry
3441
+ # @option opts [Integer] :time the number of milliseconds as a specific Unix Epoch time
3442
+ # @option opts [Integer] :retrycount the number of retry counter
3443
+ # @option opts [Boolean] :force whether to create the pending entry to the pending entries list or not
3444
+ # @option opts [Boolean] :justid whether to fetch just an array of entry ids or not
3445
+ #
3446
+ # @return [Hash{String => Hash}] the entries successfully claimed
3447
+ # @return [Array<String>] the entry ids successfully claimed if justid option is `true`
3448
+ def xclaim(key, group, consumer, min_idle_time, *ids, **opts)
3449
+ args = [:xclaim, key, group, consumer, min_idle_time].concat(ids.flatten)
3450
+ args.concat(['IDLE', opts[:idle].to_i]) if opts[:idle]
3451
+ args.concat(['TIME', opts[:time].to_i]) if opts[:time]
3452
+ args.concat(['RETRYCOUNT', opts[:retrycount]]) if opts[:retrycount]
3453
+ args << 'FORCE' if opts[:force]
3454
+ args << 'JUSTID' if opts[:justid]
3455
+ blk = opts[:justid] ? Noop : HashifyStreamEntries
3456
+ synchronize { |client| client.call(args, &blk) }
3457
+ end
3458
+
3459
+ # Transfers ownership of pending stream entries that match the specified criteria.
3460
+ #
3461
+ # @example Claim next pending message stuck > 5 minutes and mark as retry
3462
+ # redis.xautoclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0')
3463
+ # @example Claim 50 next pending messages stuck > 5 minutes and mark as retry
3464
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0', count: 50)
3465
+ # @example Claim next pending message stuck > 5 minutes and don't mark as retry
3466
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-0', justid: true)
3467
+ # @example Claim next pending message after this id stuck > 5 minutes and mark as retry
3468
+ # redis.xautoclaim('mystream', 'mygroup', 'consumer1', 3600000, '1641321233-0')
3469
+ #
3470
+ # @param key [String] the stream key
3471
+ # @param group [String] the consumer group name
3472
+ # @param consumer [String] the consumer name
3473
+ # @param min_idle_time [Integer] the number of milliseconds
3474
+ # @param start [String] entry id to start scanning from or 0-0 for everything
3475
+ # @param count [Integer] number of messages to claim (default 1)
3476
+ # @param justid [Boolean] whether to fetch just an array of entry ids or not.
3477
+ # Does not increment retry count when true
3478
+ #
3479
+ # @return [Hash{String => Hash}] the entries successfully claimed
3480
+ # @return [Array<String>] the entry ids successfully claimed if justid option is `true`
3481
+ def xautoclaim(key, group, consumer, min_idle_time, start, count: nil, justid: false)
3482
+ args = [:xautoclaim, key, group, consumer, min_idle_time, start]
3483
+ if count
3484
+ args << 'COUNT' << count.to_s
3485
+ end
3486
+ args << 'JUSTID' if justid
3487
+ blk = justid ? HashifyStreamAutoclaimJustId : HashifyStreamAutoclaim
3488
+ synchronize { |client| client.call(args, &blk) }
3489
+ end
3490
+
3491
+ # Fetches not acknowledging pending entries
3492
+ #
3493
+ # @example With key and group
3494
+ # redis.xpending('mystream', 'mygroup')
3495
+ # @example With range options
3496
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10)
3497
+ # @example With range and consumer options
3498
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10, 'consumer1')
3499
+ #
3500
+ # @param key [String] the stream key
3501
+ # @param group [String] the consumer group name
3502
+ # @param start [String] start first entry id of range
3503
+ # @param end [String] end last entry id of range
3504
+ # @param count [Integer] count the number of entries as limit
3505
+ # @param consumer [String] the consumer name
3506
+ #
3507
+ # @return [Hash] the summary of pending entries
3508
+ # @return [Array<Hash>] the pending entries details if options were specified
3509
+ def xpending(key, group, *args)
3510
+ command_args = [:xpending, key, group]
3511
+ case args.size
3512
+ when 0, 3, 4
3513
+ command_args.concat(args)
3514
+ else
3515
+ raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 2, 5 or 6)"
3516
+ end
3517
+
3518
+ summary_needed = args.empty?
3519
+ blk = summary_needed ? HashifyStreamPendings : HashifyStreamPendingDetails
3520
+ synchronize { |client| client.call(command_args, &blk) }
3521
+ end
3522
+
2664
3523
  # Interact with the sentinel command (masters, master, slaves, failover)
2665
3524
  #
2666
3525
  # @param [String] subcommand e.g. `masters`, `master`, `slaves`
@@ -2674,8 +3533,8 @@ class Redis
2674
3533
  when "get-master-addr-by-name"
2675
3534
  reply
2676
3535
  else
2677
- if reply.kind_of?(Array)
2678
- if reply[0].kind_of?(Array)
3536
+ if reply.is_a?(Array)
3537
+ if reply[0].is_a?(Array)
2679
3538
  reply.map(&Hashify)
2680
3539
  else
2681
3540
  Hashify.call(reply)
@@ -2688,6 +3547,46 @@ class Redis
2688
3547
  end
2689
3548
  end
2690
3549
 
3550
+ # Sends `CLUSTER *` command to random node and returns its reply.
3551
+ #
3552
+ # @see https://redis.io/commands#cluster Reference of cluster command
3553
+ #
3554
+ # @param subcommand [String, Symbol] the subcommand of cluster command
3555
+ # e.g. `:slots`, `:nodes`, `:slaves`, `:info`
3556
+ #
3557
+ # @return [Object] depends on the subcommand
3558
+ def cluster(subcommand, *args)
3559
+ subcommand = subcommand.to_s.downcase
3560
+ block = case subcommand
3561
+ when 'slots'
3562
+ HashifyClusterSlots
3563
+ when 'nodes'
3564
+ HashifyClusterNodes
3565
+ when 'slaves'
3566
+ HashifyClusterSlaves
3567
+ when 'info'
3568
+ HashifyInfo
3569
+ else
3570
+ Noop
3571
+ end
3572
+
3573
+ # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
3574
+ block = Noop unless @cluster_mode
3575
+
3576
+ synchronize do |client|
3577
+ client.call([:cluster, subcommand] + args, &block)
3578
+ end
3579
+ end
3580
+
3581
+ # Sends `ASKING` command to random node and returns its reply.
3582
+ #
3583
+ # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
3584
+ #
3585
+ # @return [String] `'OK'`
3586
+ def asking
3587
+ synchronize { |client| client.call(%i[asking]) }
3588
+ end
3589
+
2691
3590
  def id
2692
3591
  @original_client.id
2693
3592
  end
@@ -2701,68 +3600,183 @@ class Redis
2701
3600
  end
2702
3601
 
2703
3602
  def connection
3603
+ return @original_client.connection_info if @cluster_mode
3604
+
2704
3605
  {
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
3606
+ host: @original_client.host,
3607
+ port: @original_client.port,
3608
+ db: @original_client.db,
3609
+ id: @original_client.id,
3610
+ location: @original_client.location
2710
3611
  }
2711
3612
  end
2712
3613
 
2713
- def method_missing(command, *args)
3614
+ def method_missing(command, *args) # rubocop:disable Style/MissingRespondToMissing
2714
3615
  synchronize do |client|
2715
3616
  client.call([command] + args)
2716
3617
  end
2717
3618
  end
2718
3619
 
2719
- private
3620
+ private
2720
3621
 
2721
3622
  # Commands returning 1 for true and 0 for false may be executed in a pipeline
2722
3623
  # where the method call will return nil. Propagate the nil instead of falsely
2723
3624
  # returning false.
2724
- Boolify =
2725
- lambda { |value|
2726
- value == 1 if value
2727
- }
3625
+ Boolify = lambda { |value|
3626
+ case value
3627
+ when 1
3628
+ true
3629
+ when 0
3630
+ false
3631
+ else
3632
+ value
3633
+ end
3634
+ }
2728
3635
 
2729
- BoolifySet =
2730
- lambda { |value|
2731
- if value && "OK" == value
2732
- true
2733
- else
2734
- false
2735
- end
3636
+ BoolifySet = lambda { |value|
3637
+ case value
3638
+ when "OK"
3639
+ true
3640
+ when nil
3641
+ false
3642
+ else
3643
+ value
3644
+ end
3645
+ }
3646
+
3647
+ Hashify = lambda { |value|
3648
+ if value.respond_to?(:each_slice)
3649
+ value.each_slice(2).to_h
3650
+ else
3651
+ value
3652
+ end
3653
+ }
3654
+
3655
+ Floatify = lambda { |value|
3656
+ case value
3657
+ when "inf"
3658
+ Float::INFINITY
3659
+ when "-inf"
3660
+ -Float::INFINITY
3661
+ when String
3662
+ Float(value)
3663
+ else
3664
+ value
3665
+ end
3666
+ }
3667
+
3668
+ FloatifyPairs = lambda { |value|
3669
+ return value unless value.respond_to?(:each_slice)
3670
+
3671
+ value.each_slice(2).map do |member, score|
3672
+ [member, Floatify.call(score)]
3673
+ end
3674
+ }
3675
+
3676
+ HashifyInfo = lambda { |reply|
3677
+ lines = reply.split("\r\n").grep_v(/^(#|$)/)
3678
+ lines.map! { |line| line.split(':', 2) }
3679
+ lines.compact!
3680
+ lines.to_h
3681
+ }
3682
+
3683
+ HashifyStreams = lambda { |reply|
3684
+ case reply
3685
+ when nil
3686
+ {}
3687
+ else
3688
+ reply.map { |key, entries| [key, HashifyStreamEntries.call(entries)] }.to_h
3689
+ end
3690
+ }
3691
+
3692
+ EMPTY_STREAM_RESPONSE = [nil].freeze
3693
+ private_constant :EMPTY_STREAM_RESPONSE
3694
+
3695
+ HashifyStreamEntries = lambda { |reply|
3696
+ reply.compact.map do |entry_id, values|
3697
+ [entry_id, values&.each_slice(2)&.to_h]
3698
+ end
3699
+ }
3700
+
3701
+ HashifyStreamAutoclaim = lambda { |reply|
3702
+ {
3703
+ 'next' => reply[0],
3704
+ 'entries' => reply[1].map { |entry| [entry[0], entry[1].each_slice(2).to_h] }
2736
3705
  }
3706
+ }
2737
3707
 
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
3708
+ HashifyStreamAutoclaimJustId = lambda { |reply|
3709
+ {
3710
+ 'next' => reply[0],
3711
+ 'entries' => reply[1]
2745
3712
  }
3713
+ }
2746
3714
 
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
3715
+ HashifyStreamPendings = lambda { |reply|
3716
+ {
3717
+ 'size' => reply[0],
3718
+ 'min_entry_id' => reply[1],
3719
+ 'max_entry_id' => reply[2],
3720
+ 'consumers' => reply[3].nil? ? {} : reply[3].to_h
2756
3721
  }
3722
+ }
2757
3723
 
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
3724
+ HashifyStreamPendingDetails = lambda { |reply|
3725
+ reply.map do |arr|
3726
+ {
3727
+ 'entry_id' => arr[0],
3728
+ 'consumer' => arr[1],
3729
+ 'elapsed' => arr[2],
3730
+ 'count' => arr[3]
3731
+ }
3732
+ end
3733
+ }
3734
+
3735
+ HashifyClusterNodeInfo = lambda { |str|
3736
+ arr = str.split(' ')
3737
+ {
3738
+ 'node_id' => arr[0],
3739
+ 'ip_port' => arr[1],
3740
+ 'flags' => arr[2].split(','),
3741
+ 'master_node_id' => arr[3],
3742
+ 'ping_sent' => arr[4],
3743
+ 'pong_recv' => arr[5],
3744
+ 'config_epoch' => arr[6],
3745
+ 'link_state' => arr[7],
3746
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
2765
3747
  }
3748
+ }
3749
+
3750
+ HashifyClusterSlots = lambda { |reply|
3751
+ reply.map do |arr|
3752
+ first_slot, last_slot = arr[0..1]
3753
+ master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
3754
+ replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
3755
+ {
3756
+ 'start_slot' => first_slot,
3757
+ 'end_slot' => last_slot,
3758
+ 'master' => master,
3759
+ 'replicas' => replicas
3760
+ }
3761
+ end
3762
+ }
3763
+
3764
+ HashifyClusterNodes = lambda { |reply|
3765
+ reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
3766
+ }
3767
+
3768
+ HashifyClusterSlaves = lambda { |reply|
3769
+ reply.map { |str| HashifyClusterNodeInfo.call(str) }
3770
+ }
3771
+
3772
+ Noop = ->(reply) { reply }
3773
+
3774
+ def _geoarguments(*args, options: nil, sort: nil, count: nil)
3775
+ args.push sort if sort
3776
+ args.push 'count', count if count
3777
+ args.push options if options
3778
+ args
3779
+ end
2766
3780
 
2767
3781
  def _subscription(method, timeout, channels, block)
2768
3782
  return @client.call([method] + channels) if subscribed?
@@ -2779,10 +3793,44 @@ private
2779
3793
  end
2780
3794
  end
2781
3795
 
3796
+ def _xread(args, keys, ids, blocking_timeout_msec)
3797
+ keys = keys.is_a?(Array) ? keys : [keys]
3798
+ ids = ids.is_a?(Array) ? ids : [ids]
3799
+ args << 'STREAMS'
3800
+ args.concat(keys)
3801
+ args.concat(ids)
3802
+
3803
+ synchronize do |client|
3804
+ if blocking_timeout_msec.nil?
3805
+ client.call(args, &HashifyStreams)
3806
+ elsif blocking_timeout_msec.to_f.zero?
3807
+ client.call_without_timeout(args, &HashifyStreams)
3808
+ else
3809
+ timeout = client.timeout.to_f + blocking_timeout_msec.to_f / 1000.0
3810
+ client.call_with_timeout(args, timeout, &HashifyStreams)
3811
+ end
3812
+ end
3813
+ end
3814
+
3815
+ def _normalize_move_wheres(where_source, where_destination)
3816
+ where_source = where_source.to_s.upcase
3817
+ where_destination = where_destination.to_s.upcase
3818
+
3819
+ if where_source != "LEFT" && where_source != "RIGHT"
3820
+ raise ArgumentError, "where_source must be 'LEFT' or 'RIGHT'"
3821
+ end
3822
+
3823
+ if where_destination != "LEFT" && where_destination != "RIGHT"
3824
+ raise ArgumentError, "where_destination must be 'LEFT' or 'RIGHT'"
3825
+ end
3826
+
3827
+ [where_source, where_destination]
3828
+ end
2782
3829
  end
2783
3830
 
2784
- require "redis/version"
2785
- require "redis/connection"
2786
- require "redis/client"
2787
- require "redis/pipeline"
2788
- require "redis/subscribe"
3831
+ require_relative "redis/version"
3832
+ require_relative "redis/connection"
3833
+ require_relative "redis/client"
3834
+ require_relative "redis/cluster"
3835
+ require_relative "redis/pipeline"
3836
+ require_relative "redis/subscribe"