redis 3.0.0 → 4.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +315 -0
  3. data/README.md +301 -58
  4. data/lib/redis/client.rb +383 -88
  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 +12 -8
  17. data/lib/redis/connection/registry.rb +2 -1
  18. data/lib/redis/connection/ruby.rb +266 -74
  19. data/lib/redis/connection/synchrony.rb +41 -14
  20. data/lib/redis/connection.rb +4 -2
  21. data/lib/redis/distributed.rb +258 -76
  22. data/lib/redis/errors.rb +48 -0
  23. data/lib/redis/hash_ring.rb +31 -73
  24. data/lib/redis/pipeline.rb +74 -18
  25. data/lib/redis/subscribe.rb +24 -13
  26. data/lib/redis/version.rb +3 -1
  27. data/lib/redis.rb +2068 -464
  28. metadata +63 -160
  29. data/.gitignore +0 -10
  30. data/.order +0 -169
  31. data/.travis/Gemfile +0 -11
  32. data/.travis.yml +0 -50
  33. data/.yardopts +0 -3
  34. data/Rakefile +0 -392
  35. data/benchmarking/logging.rb +0 -62
  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/dist_redis.rb +0 -43
  42. data/examples/incr-decr.rb +0 -17
  43. data/examples/list.rb +0 -26
  44. data/examples/pubsub.rb +0 -31
  45. data/examples/sets.rb +0 -36
  46. data/examples/unicorn/config.ru +0 -3
  47. data/examples/unicorn/unicorn.rb +0 -20
  48. data/redis.gemspec +0 -41
  49. data/test/blocking_commands_test.rb +0 -42
  50. data/test/command_map_test.rb +0 -30
  51. data/test/commands_on_hashes_test.rb +0 -21
  52. data/test/commands_on_lists_test.rb +0 -20
  53. data/test/commands_on_sets_test.rb +0 -77
  54. data/test/commands_on_sorted_sets_test.rb +0 -109
  55. data/test/commands_on_strings_test.rb +0 -83
  56. data/test/commands_on_value_types_test.rb +0 -99
  57. data/test/connection_handling_test.rb +0 -189
  58. data/test/db/.gitignore +0 -1
  59. data/test/distributed_blocking_commands_test.rb +0 -46
  60. data/test/distributed_commands_on_hashes_test.rb +0 -10
  61. data/test/distributed_commands_on_lists_test.rb +0 -22
  62. data/test/distributed_commands_on_sets_test.rb +0 -83
  63. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  64. data/test/distributed_commands_on_strings_test.rb +0 -48
  65. data/test/distributed_commands_on_value_types_test.rb +0 -87
  66. data/test/distributed_commands_requiring_clustering_test.rb +0 -148
  67. data/test/distributed_connection_handling_test.rb +0 -23
  68. data/test/distributed_internals_test.rb +0 -15
  69. data/test/distributed_key_tags_test.rb +0 -52
  70. data/test/distributed_persistence_control_commands_test.rb +0 -26
  71. data/test/distributed_publish_subscribe_test.rb +0 -92
  72. data/test/distributed_remote_server_control_commands_test.rb +0 -53
  73. data/test/distributed_scripting_test.rb +0 -102
  74. data/test/distributed_sorting_test.rb +0 -20
  75. data/test/distributed_test.rb +0 -58
  76. data/test/distributed_transactions_test.rb +0 -32
  77. data/test/encoding_test.rb +0 -18
  78. data/test/error_replies_test.rb +0 -59
  79. data/test/helper.rb +0 -188
  80. data/test/helper_test.rb +0 -22
  81. data/test/internals_test.rb +0 -214
  82. data/test/lint/blocking_commands.rb +0 -124
  83. data/test/lint/hashes.rb +0 -162
  84. data/test/lint/lists.rb +0 -143
  85. data/test/lint/sets.rb +0 -96
  86. data/test/lint/sorted_sets.rb +0 -201
  87. data/test/lint/strings.rb +0 -157
  88. data/test/lint/value_types.rb +0 -106
  89. data/test/persistence_control_commands_test.rb +0 -26
  90. data/test/pipelining_commands_test.rb +0 -195
  91. data/test/publish_subscribe_test.rb +0 -153
  92. data/test/remote_server_control_commands_test.rb +0 -104
  93. data/test/scripting_test.rb +0 -78
  94. data/test/sorting_test.rb +0 -45
  95. data/test/support/connection/hiredis.rb +0 -1
  96. data/test/support/connection/ruby.rb +0 -1
  97. data/test/support/connection/synchrony.rb +0 -17
  98. data/test/support/redis_mock.rb +0 -92
  99. data/test/support/wire/synchrony.rb +0 -24
  100. data/test/support/wire/thread.rb +0 -5
  101. data/test/synchrony_driver.rb +0 -57
  102. data/test/test.conf +0 -9
  103. data/test/thread_safety_test.rb +0 -32
  104. data/test/transactions_test.rb +0 -244
  105. data/test/unknown_commands_test.rb +0 -14
  106. data/test/url_param_test.rb +0 -64
data/lib/redis.rb CHANGED
@@ -1,33 +1,69 @@
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
11
+
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
9
20
 
10
- attr :client
21
+ @exists_returns_integer = value
22
+ end
11
23
 
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)
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
 
33
+ # Create a new client instance
34
+ #
35
+ # @param [Hash] 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.
39
+ # @option options [String] :host ("127.0.0.1") server hostname
40
+ # @option options [Integer] :port (6379) server port
41
+ # @option options [String] :path path to server socket (overrides host and port)
42
+ # @option options [Float] :timeout (5.0) timeout in seconds
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
45
+ # @option options [String] :password Password to authenticate against server
46
+ # @option options [Integer] :db (0) Database to select after initial connect
47
+ # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`, `:synchrony`
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
53
+ # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
54
+ # @option options [Array] :sentinels List of sentinels to contact
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
59
+ #
60
+ # @return [Redis] a new client instance
29
61
  def initialize(options = {})
30
- @client = Client.new(options)
62
+ @options = options.dup
63
+ @cluster_mode = options.key?(:cluster)
64
+ client = @cluster_mode ? Cluster : Client
65
+ @original_client = @client = client.new(options)
66
+ @queue = Hash.new { |h, k| h[k] = [] }
31
67
 
32
68
  super() # Monitor#initialize
33
69
  end
@@ -37,7 +73,7 @@ class Redis
37
73
  end
38
74
 
39
75
  # Run code with the client reconnecting
40
- def with_reconnect(val=true, &blk)
76
+ def with_reconnect(val = true, &blk)
41
77
  synchronize do |client|
42
78
  client.with_reconnect(val, &blk)
43
79
  end
@@ -48,34 +84,96 @@ class Redis
48
84
  with_reconnect(false, &blk)
49
85
  end
50
86
 
87
+ # Test whether or not the client is connected
88
+ def connected?
89
+ @original_client.connected?
90
+ end
91
+
92
+ # Disconnect the client as quickly and silently as possible.
93
+ def close
94
+ @original_client.disconnect
95
+ end
96
+ alias disconnect! close
97
+
98
+ # Sends a command to Redis and returns its reply.
99
+ #
100
+ # Replies are converted to Ruby objects according to the RESP protocol, so
101
+ # you can expect a Ruby array, integer or nil when Redis sends one. Higher
102
+ # level transformations, such as converting an array of pairs into a Ruby
103
+ # hash, are up to consumers.
104
+ #
105
+ # Redis error replies are raised as Ruby exceptions.
106
+ def call(*command)
107
+ synchronize do |client|
108
+ client.call(command)
109
+ end
110
+ end
111
+
112
+ # Queues a command for pipelining.
113
+ #
114
+ # Commands in the queue are executed with the Redis#commit method.
115
+ #
116
+ # See http://redis.io/topics/pipelining for more details.
117
+ #
118
+ def queue(*command)
119
+ synchronize do
120
+ @queue[Thread.current.object_id] << command
121
+ end
122
+ end
123
+
124
+ # Sends all commands in the queue.
125
+ #
126
+ # See http://redis.io/topics/pipelining for more details.
127
+ #
128
+ def commit
129
+ synchronize do |client|
130
+ begin
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)
137
+ ensure
138
+ @queue.delete(Thread.current.object_id)
139
+ end
140
+ end
141
+ end
142
+
143
+ def _client
144
+ @client
145
+ end
146
+
51
147
  # Authenticate to the server.
52
148
  #
53
- # @param [String] password must match the password specified in the
54
- # `requirepass` directive in the configuration file
149
+ # @param [Array<String>] args includes both username and password
150
+ # or only password
55
151
  # @return [String] `OK`
56
- def auth(password)
152
+ # @see https://redis.io/commands/auth AUTH command
153
+ def auth(*args)
57
154
  synchronize do |client|
58
- client.call [:auth, password]
155
+ client.call([:auth, *args])
59
156
  end
60
157
  end
61
158
 
62
159
  # Change the selected database for the current connection.
63
160
  #
64
- # @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)
65
162
  # @return [String] `OK`
66
163
  def select(db)
67
164
  synchronize do |client|
68
165
  client.db = db
69
- client.call [:select, db]
166
+ client.call([:select, db])
70
167
  end
71
168
  end
72
169
 
73
170
  # Ping the server.
74
171
  #
172
+ # @param [optional, String] message
75
173
  # @return [String] `PONG`
76
- def ping
174
+ def ping(message = nil)
77
175
  synchronize do |client|
78
- client.call [:ping]
176
+ client.call([:ping, message].compact)
79
177
  end
80
178
  end
81
179
 
@@ -85,7 +183,7 @@ class Redis
85
183
  # @return [String]
86
184
  def echo(value)
87
185
  synchronize do |client|
88
- client.call [:echo, value]
186
+ client.call([:echo, value])
89
187
  end
90
188
  end
91
189
 
@@ -95,7 +193,7 @@ class Redis
95
193
  def quit
96
194
  synchronize do |client|
97
195
  begin
98
- client.call [:quit]
196
+ client.call([:quit])
99
197
  rescue ConnectionError
100
198
  ensure
101
199
  client.disconnect
@@ -108,7 +206,7 @@ class Redis
108
206
  # @return [String] `OK`
109
207
  def bgrewriteaof
110
208
  synchronize do |client|
111
- client.call [:bgrewriteaof]
209
+ client.call([:bgrewriteaof])
112
210
  end
113
211
  end
114
212
 
@@ -117,20 +215,39 @@ class Redis
117
215
  # @return [String] `OK`
118
216
  def bgsave
119
217
  synchronize do |client|
120
- client.call [:bgsave]
218
+ client.call([:bgsave])
121
219
  end
122
220
  end
123
221
 
124
222
  # Get or set server configuration parameters.
125
223
  #
126
- # @param [String] action e.g. `get`, `set`, `resetstat`
224
+ # @param [Symbol] action e.g. `:get`, `:set`, `:resetstat`
127
225
  # @return [String, Hash] string reply, or hash when retrieving more than one
128
226
  # property with `CONFIG GET`
129
227
  def config(action, *args)
130
228
  synchronize do |client|
131
- client.call [:config, action, *args] do |reply|
132
- if reply.kind_of?(Array) && action == :get
133
- Hash[*reply]
229
+ client.call([:config, action] + args) do |reply|
230
+ if reply.is_a?(Array) && action == :get
231
+ Hashify.call(reply)
232
+ else
233
+ reply
234
+ end
235
+ end
236
+ end
237
+ end
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
134
251
  else
135
252
  reply
136
253
  end
@@ -140,34 +257,46 @@ class Redis
140
257
 
141
258
  # Return the number of keys in the selected database.
142
259
  #
143
- # @return [Fixnum]
260
+ # @return [Integer]
144
261
  def dbsize
145
262
  synchronize do |client|
146
- client.call [:dbsize]
263
+ client.call([:dbsize])
147
264
  end
148
265
  end
149
266
 
150
267
  def debug(*args)
151
268
  synchronize do |client|
152
- client.call [:debug, *args]
269
+ client.call([:debug] + args)
153
270
  end
154
271
  end
155
272
 
156
273
  # Remove all keys from all databases.
157
274
  #
275
+ # @param [Hash] options
276
+ # - `:async => Boolean`: async flush (default: false)
158
277
  # @return [String] `OK`
159
- def flushall
278
+ def flushall(options = nil)
160
279
  synchronize do |client|
161
- client.call [:flushall]
280
+ if options && options[:async]
281
+ client.call(%i[flushall async])
282
+ else
283
+ client.call([:flushall])
284
+ end
162
285
  end
163
286
  end
164
287
 
165
288
  # Remove all keys from the current database.
166
289
  #
290
+ # @param [Hash] options
291
+ # - `:async => Boolean`: async flush (default: false)
167
292
  # @return [String] `OK`
168
- def flushdb
293
+ def flushdb(options = nil)
169
294
  synchronize do |client|
170
- client.call [:flushdb]
295
+ if options && options[:async]
296
+ client.call(%i[flushdb async])
297
+ else
298
+ client.call([:flushdb])
299
+ end
171
300
  end
172
301
  end
173
302
 
@@ -177,16 +306,15 @@ class Redis
177
306
  # @return [Hash<String, String>]
178
307
  def info(cmd = nil)
179
308
  synchronize do |client|
180
- client.call [:info, cmd].compact do |reply|
181
- if reply.kind_of?(String)
182
- reply = Hash[reply.split("\r\n").map do |line|
183
- line.split(":", 2) unless line =~ /^(#|$)/
184
- end]
309
+ client.call([:info, cmd].compact) do |reply|
310
+ if reply.is_a?(String)
311
+ reply = HashifyInfo.call(reply)
185
312
 
186
313
  if cmd && cmd.to_s == "commandstats"
187
314
  # Extract nested hashes for INFO COMMANDSTATS
188
315
  reply = Hash[reply.map do |k, v|
189
- [k[/^cmdstat_(.*)$/, 1], Hash[*v.split(/,|=/)]]
316
+ v = v.split(",").map { |e| e.split("=") }
317
+ [k[/^cmdstat_(.*)$/, 1], Hash[v]]
190
318
  end]
191
319
  end
192
320
  end
@@ -198,10 +326,10 @@ class Redis
198
326
 
199
327
  # Get the UNIX time stamp of the last successful save to disk.
200
328
  #
201
- # @return [Fixnum]
329
+ # @return [Integer]
202
330
  def lastsave
203
331
  synchronize do |client|
204
- client.call [:lastsave]
332
+ client.call([:lastsave])
205
333
  end
206
334
  end
207
335
 
@@ -222,7 +350,7 @@ class Redis
222
350
  # @return [String]
223
351
  def save
224
352
  synchronize do |client|
225
- client.call [:save]
353
+ client.call([:save])
226
354
  end
227
355
  end
228
356
 
@@ -231,7 +359,7 @@ class Redis
231
359
  synchronize do |client|
232
360
  client.with_reconnect(false) do
233
361
  begin
234
- client.call [:shutdown]
362
+ client.call([:shutdown])
235
363
  rescue ConnectionError
236
364
  # This means Redis has probably exited.
237
365
  nil
@@ -243,16 +371,16 @@ class Redis
243
371
  # Make the server a slave of another instance, or promote it as master.
244
372
  def slaveof(host, port)
245
373
  synchronize do |client|
246
- client.call [:slaveof, host, port]
374
+ client.call([:slaveof, host, port])
247
375
  end
248
376
  end
249
377
 
250
378
  # Interact with the slowlog (get, len, reset)
251
379
  #
252
380
  # @param [String] subcommand e.g. `get`, `len`, `reset`
253
- # @param [Fixnum] length maximum number of entries to return
254
- # @return [Array<String>, Fixnum, String] depends on subcommand
255
- 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)
256
384
  synchronize do |client|
257
385
  args = [:slowlog, subcommand]
258
386
  args << length if length
@@ -263,7 +391,7 @@ class Redis
263
391
  # Internal command used for replication.
264
392
  def sync
265
393
  synchronize do |client|
266
- client.call [:sync]
394
+ client.call([:sync])
267
395
  end
268
396
  end
269
397
 
@@ -272,12 +400,12 @@ class Redis
272
400
  # @example
273
401
  # r.time # => [ 1333093196, 606806 ]
274
402
  #
275
- # @return [Array<Fixnum>] tuple of seconds since UNIX epoch and
403
+ # @return [Array<Integer>] tuple of seconds since UNIX epoch and
276
404
  # microseconds in the current second
277
405
  def time
278
406
  synchronize do |client|
279
- client.call [:time] do |reply|
280
- reply.map(&:to_i) if reply
407
+ client.call([:time]) do |reply|
408
+ reply&.map(&:to_i)
281
409
  end
282
410
  end
283
411
  end
@@ -288,93 +416,202 @@ class Redis
288
416
  # @return [Boolean] whether the timeout was removed or not
289
417
  def persist(key)
290
418
  synchronize do |client|
291
- client.call [:persist, key], &_boolify
419
+ client.call([:persist, key], &Boolify)
292
420
  end
293
421
  end
294
422
 
295
423
  # Set a key's time to live in seconds.
296
424
  #
297
425
  # @param [String] key
298
- # @param [Fixnum] seconds time to live
426
+ # @param [Integer] seconds time to live
299
427
  # @return [Boolean] whether the timeout was set or not
300
428
  def expire(key, seconds)
301
429
  synchronize do |client|
302
- client.call [:expire, key, seconds], &_boolify
430
+ client.call([:expire, key, seconds], &Boolify)
303
431
  end
304
432
  end
305
433
 
306
434
  # Set the expiration for a key as a UNIX timestamp.
307
435
  #
308
436
  # @param [String] key
309
- # @param [Fixnum] unix_time expiry time specified as a UNIX timestamp
437
+ # @param [Integer] unix_time expiry time specified as a UNIX timestamp
310
438
  # @return [Boolean] whether the timeout was set or not
311
439
  def expireat(key, unix_time)
312
440
  synchronize do |client|
313
- client.call [:expireat, key, unix_time], &_boolify
441
+ client.call([:expireat, key, unix_time], &Boolify)
314
442
  end
315
443
  end
316
444
 
317
445
  # Get the time to live (in seconds) for a key.
318
446
  #
319
447
  # @param [String] key
320
- # @return [Fixnum] remaining time to live in seconds, or -1 if the
321
- # key does not exist or does not have a timeout
448
+ # @return [Integer] remaining time to live in seconds.
449
+ #
450
+ # In Redis 2.6 or older the command returns -1 if the key does not exist or if
451
+ # the key exist but has no associated expire.
452
+ #
453
+ # Starting with Redis 2.8 the return value in case of error changed:
454
+ #
455
+ # - The command returns -2 if the key does not exist.
456
+ # - The command returns -1 if the key exists but has no associated expire.
322
457
  def ttl(key)
323
458
  synchronize do |client|
324
- client.call [:ttl, key]
459
+ client.call([:ttl, key])
325
460
  end
326
461
  end
327
462
 
328
463
  # Set a key's time to live in milliseconds.
329
464
  #
330
465
  # @param [String] key
331
- # @param [Fixnum] milliseconds time to live
466
+ # @param [Integer] milliseconds time to live
332
467
  # @return [Boolean] whether the timeout was set or not
333
468
  def pexpire(key, milliseconds)
334
469
  synchronize do |client|
335
- client.call [:pexpire, key, milliseconds], &_boolify
470
+ client.call([:pexpire, key, milliseconds], &Boolify)
336
471
  end
337
472
  end
338
473
 
339
474
  # Set the expiration for a key as number of milliseconds from UNIX Epoch.
340
475
  #
341
476
  # @param [String] key
342
- # @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.
343
478
  # @return [Boolean] whether the timeout was set or not
344
479
  def pexpireat(key, ms_unix_time)
345
480
  synchronize do |client|
346
- client.call [:pexpireat, key, ms_unix_time], &_boolify
481
+ client.call([:pexpireat, key, ms_unix_time], &Boolify)
347
482
  end
348
483
  end
349
484
 
350
485
  # Get the time to live (in milliseconds) for a key.
351
486
  #
352
487
  # @param [String] key
353
- # @return [Fixnum] remaining time to live in milliseconds, or -1 if the
354
- # key does not exist or does not have a timeout
488
+ # @return [Integer] remaining time to live in milliseconds
489
+ # In Redis 2.6 or older the command returns -1 if the key does not exist or if
490
+ # the key exist but has no associated expire.
491
+ #
492
+ # Starting with Redis 2.8 the return value in case of error changed:
493
+ #
494
+ # - The command returns -2 if the key does not exist.
495
+ # - The command returns -1 if the key exists but has no associated expire.
355
496
  def pttl(key)
356
497
  synchronize do |client|
357
- client.call [:pttl, key]
498
+ client.call([:pttl, key])
499
+ end
500
+ end
501
+
502
+ # Return a serialized version of the value stored at a key.
503
+ #
504
+ # @param [String] key
505
+ # @return [String] serialized_value
506
+ def dump(key)
507
+ synchronize do |client|
508
+ client.call([:dump, key])
509
+ end
510
+ end
511
+
512
+ # Create a key using the serialized value, previously obtained using DUMP.
513
+ #
514
+ # @param [String] key
515
+ # @param [String] ttl
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]
520
+ # @return [String] `"OK"`
521
+ def restore(key, ttl, serialized_value, replace: nil)
522
+ args = [:restore, key, ttl, serialized_value]
523
+ args << 'REPLACE' if replace
524
+
525
+ synchronize do |client|
526
+ client.call(args)
358
527
  end
359
528
  end
360
529
 
530
+ # Transfer a key from the connected instance to another instance.
531
+ #
532
+ # @param [String, Array<String>] key
533
+ # @param [Hash] options
534
+ # - `:host => String`: host of instance to migrate to
535
+ # - `:port => Integer`: port of instance to migrate to
536
+ # - `:db => Integer`: database to migrate to (default: same as source)
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.
540
+ # @return [String] `"OK"`
541
+ def migrate(key, options)
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)
551
+
552
+ synchronize { |client| client.call(args) }
553
+ end
554
+
361
555
  # Delete one or more keys.
362
556
  #
363
557
  # @param [String, Array<String>] keys
364
- # @return [Fixnum] number of keys that were deleted
558
+ # @return [Integer] number of keys that were deleted
365
559
  def del(*keys)
560
+ keys.flatten!(1)
561
+ return 0 if keys.empty?
562
+
366
563
  synchronize do |client|
367
- client.call [:del, *keys]
564
+ client.call([:del] + keys)
368
565
  end
369
566
  end
370
567
 
371
- # Determine if a key exists.
568
+ # Unlink one or more keys.
372
569
  #
373
- # @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
374
609
  # @return [Boolean]
375
- def exists(key)
610
+ def exists?(*keys)
376
611
  synchronize do |client|
377
- client.call [:exists, key], &_boolify
612
+ client.call([:exists, *keys]) do |value|
613
+ value > 0
614
+ end
378
615
  end
379
616
  end
380
617
 
@@ -384,8 +621,8 @@ class Redis
384
621
  # @return [Array<String>]
385
622
  def keys(pattern = "*")
386
623
  synchronize do |client|
387
- client.call [:keys, pattern] do |reply|
388
- if reply.kind_of?(String)
624
+ client.call([:keys, pattern]) do |reply|
625
+ if reply.is_a?(String)
389
626
  reply.split(" ")
390
627
  else
391
628
  reply
@@ -407,21 +644,21 @@ class Redis
407
644
  # # => "OK"
408
645
  # redis.exists "foo"
409
646
  # # => true
410
- # resis.get "foo"
647
+ # redis.get "foo"
411
648
  # # => "bar"
412
649
  #
413
650
  # @param [String] key
414
- # @param [Fixnum] db
651
+ # @param [Integer] db
415
652
  # @return [Boolean] whether the key was moved or not
416
653
  def move(key, db)
417
654
  synchronize do |client|
418
- client.call [:move, key, db], &_boolify
655
+ client.call([:move, key, db], &Boolify)
419
656
  end
420
657
  end
421
658
 
422
659
  def object(*args)
423
660
  synchronize do |client|
424
- client.call [:object, *args]
661
+ client.call([:object] + args)
425
662
  end
426
663
  end
427
664
 
@@ -430,7 +667,7 @@ class Redis
430
667
  # @return [String]
431
668
  def randomkey
432
669
  synchronize do |client|
433
- client.call [:randomkey]
670
+ client.call([:randomkey])
434
671
  end
435
672
  end
436
673
 
@@ -441,7 +678,7 @@ class Redis
441
678
  # @return [String] `OK`
442
679
  def rename(old_name, new_name)
443
680
  synchronize do |client|
444
- client.call [:rename, old_name, new_name]
681
+ client.call([:rename, old_name, new_name])
445
682
  end
446
683
  end
447
684
 
@@ -452,7 +689,7 @@ class Redis
452
689
  # @return [Boolean] whether the key was renamed or not
453
690
  def renamenx(old_name, new_name)
454
691
  synchronize do |client|
455
- client.call [:renamenx, old_name, new_name], &_boolify
692
+ client.call([:renamenx, old_name, new_name], &Boolify)
456
693
  end
457
694
  end
458
695
 
@@ -475,36 +712,33 @@ class Redis
475
712
  # - `:order => String`: combination of `ASC`, `DESC` and optionally `ALPHA`
476
713
  # - `:store => String`: key to store the result at
477
714
  #
478
- # @return [Array<String>, Array<Array<String>>, Fixnum]
715
+ # @return [Array<String>, Array<Array<String>>, Integer]
479
716
  # - when `:get` is not specified, or holds a single element, an array of elements
480
717
  # - when `:get` is specified, and holds more than one element, an array of
481
718
  # elements where every element is an array with the result for every
482
719
  # element specified in `:get`
483
720
  # - when `:store` is specified, the number of elements in the stored result
484
- def sort(key, options = {})
485
- 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
486
724
 
487
- by = options[:by]
488
- args.concat ["BY", by] if by
489
-
490
- limit = options[:limit]
491
- args.concat ["LIMIT", *limit] if limit
492
-
493
- get = Array(options[:get])
494
- args.concat ["GET"].product(get).flatten unless get.empty?
725
+ if limit
726
+ args << "LIMIT"
727
+ args.concat(limit)
728
+ end
495
729
 
496
- order = options[:order]
497
- args.concat order.split(" ") if order
730
+ get = Array(get)
731
+ get.each do |item|
732
+ args << "GET" << item
733
+ end
498
734
 
499
- store = options[:store]
500
- args.concat ["STORE", store] if store
735
+ args.concat(order.split(" ")) if order
736
+ args << "STORE" << store if store
501
737
 
502
738
  synchronize do |client|
503
- client.call [:sort, key, *args] do |reply|
504
- if get.size > 1
505
- if reply
506
- reply.each_slice(get.size).to_a
507
- end
739
+ client.call(args) do |reply|
740
+ if get.size > 1 && !store
741
+ reply.each_slice(get.size).to_a if reply
508
742
  else
509
743
  reply
510
744
  end
@@ -518,7 +752,7 @@ class Redis
518
752
  # @return [String] `string`, `list`, `set`, `zset`, `hash` or `none`
519
753
  def type(key)
520
754
  synchronize do |client|
521
- client.call [:type, key]
755
+ client.call([:type, key])
522
756
  end
523
757
  end
524
758
 
@@ -529,10 +763,10 @@ class Redis
529
763
  # # => 4
530
764
  #
531
765
  # @param [String] key
532
- # @return [Fixnum] value after decrementing it
766
+ # @return [Integer] value after decrementing it
533
767
  def decr(key)
534
768
  synchronize do |client|
535
- client.call [:decr, key]
769
+ client.call([:decr, key])
536
770
  end
537
771
  end
538
772
 
@@ -543,11 +777,11 @@ class Redis
543
777
  # # => 0
544
778
  #
545
779
  # @param [String] key
546
- # @param [Fixnum] decrement
547
- # @return [Fixnum] value after decrementing it
780
+ # @param [Integer] decrement
781
+ # @return [Integer] value after decrementing it
548
782
  def decrby(key, decrement)
549
783
  synchronize do |client|
550
- client.call [:decrby, key, decrement]
784
+ client.call([:decrby, key, decrement])
551
785
  end
552
786
  end
553
787
 
@@ -558,10 +792,10 @@ class Redis
558
792
  # # => 6
559
793
  #
560
794
  # @param [String] key
561
- # @return [Fixnum] value after incrementing it
795
+ # @return [Integer] value after incrementing it
562
796
  def incr(key)
563
797
  synchronize do |client|
564
- client.call [:incr, key]
798
+ client.call([:incr, key])
565
799
  end
566
800
  end
567
801
 
@@ -572,11 +806,11 @@ class Redis
572
806
  # # => 10
573
807
  #
574
808
  # @param [String] key
575
- # @param [Fixnum] increment
576
- # @return [Fixnum] value after incrementing it
809
+ # @param [Integer] increment
810
+ # @return [Integer] value after incrementing it
577
811
  def incrby(key, increment)
578
812
  synchronize do |client|
579
- client.call [:incrby, key, increment]
813
+ client.call([:incrby, key, increment])
580
814
  end
581
815
  end
582
816
 
@@ -591,9 +825,7 @@ class Redis
591
825
  # @return [Float] value after incrementing it
592
826
  def incrbyfloat(key, increment)
593
827
  synchronize do |client|
594
- client.call [:incrbyfloat, key, increment] do |reply|
595
- Float(reply) if reply
596
- end
828
+ client.call([:incrbyfloat, key, increment], &Floatify)
597
829
  end
598
830
  end
599
831
 
@@ -601,36 +833,57 @@ class Redis
601
833
  #
602
834
  # @param [String] key
603
835
  # @param [String] value
604
- # @return `"OK"`
605
- def set(key, value)
606
- synchronize do |client|
607
- client.call [:set, key, value]
836
+ # @param [Hash] options
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.
841
+ # - `:nx => true`: Only set the key if it does not already exist.
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.
845
+ # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
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
856
+
857
+ synchronize do |client|
858
+ if nx || xx
859
+ client.call(args, &BoolifySet)
860
+ else
861
+ client.call(args)
862
+ end
608
863
  end
609
864
  end
610
865
 
611
- alias :[]= :set
612
-
613
866
  # Set the time to live in seconds of a key.
614
867
  #
615
868
  # @param [String] key
616
- # @param [Fixnum] ttl
869
+ # @param [Integer] ttl
617
870
  # @param [String] value
618
- # @return `"OK"`
871
+ # @return [String] `"OK"`
619
872
  def setex(key, ttl, value)
620
873
  synchronize do |client|
621
- client.call [:setex, key, ttl, value]
874
+ client.call([:setex, key, ttl, value.to_s])
622
875
  end
623
876
  end
624
877
 
625
878
  # Set the time to live in milliseconds of a key.
626
879
  #
627
880
  # @param [String] key
628
- # @param [Fixnum] ttl
881
+ # @param [Integer] ttl
629
882
  # @param [String] value
630
- # @return `"OK"`
883
+ # @return [String] `"OK"`
631
884
  def psetex(key, ttl, value)
632
885
  synchronize do |client|
633
- client.call [:psetex, key, ttl, value]
886
+ client.call([:psetex, key, ttl, value.to_s])
634
887
  end
635
888
  end
636
889
 
@@ -641,7 +894,7 @@ class Redis
641
894
  # @return [Boolean] whether the key was set or not
642
895
  def setnx(key, value)
643
896
  synchronize do |client|
644
- client.call [:setnx, key, value], &_boolify
897
+ client.call([:setnx, key, value.to_s], &Boolify)
645
898
  end
646
899
  end
647
900
 
@@ -652,12 +905,12 @@ class Redis
652
905
  # # => "OK"
653
906
  #
654
907
  # @param [Array<String>] args array of keys and values
655
- # @return `"OK"`
908
+ # @return [String] `"OK"`
656
909
  #
657
910
  # @see #mapped_mset
658
911
  def mset(*args)
659
912
  synchronize do |client|
660
- client.call [:mset, *args]
913
+ client.call([:mset] + args)
661
914
  end
662
915
  end
663
916
 
@@ -668,11 +921,11 @@ class Redis
668
921
  # # => "OK"
669
922
  #
670
923
  # @param [Hash] hash keys mapping to values
671
- # @return `"OK"`
924
+ # @return [String] `"OK"`
672
925
  #
673
926
  # @see #mset
674
927
  def mapped_mset(hash)
675
- mset(*hash.to_a.flatten)
928
+ mset(hash.to_a.flatten)
676
929
  end
677
930
 
678
931
  # Set one or more values, only if none of the keys exist.
@@ -687,14 +940,14 @@ class Redis
687
940
  # @see #mapped_msetnx
688
941
  def msetnx(*args)
689
942
  synchronize do |client|
690
- client.call [:msetnx, *args], &_boolify
943
+ client.call([:msetnx, *args], &Boolify)
691
944
  end
692
945
  end
693
946
 
694
947
  # Set one or more values, only if none of the keys exist.
695
948
  #
696
949
  # @example
697
- # redis.msetnx({ "key1" => "v1", "key2" => "v2" })
950
+ # redis.mapped_msetnx({ "key1" => "v1", "key2" => "v2" })
698
951
  # # => true
699
952
  #
700
953
  # @param [Hash] hash keys mapping to values
@@ -702,7 +955,7 @@ class Redis
702
955
  #
703
956
  # @see #msetnx
704
957
  def mapped_msetnx(hash)
705
- msetnx(*hash.to_a.flatten)
958
+ msetnx(hash.to_a.flatten)
706
959
  end
707
960
 
708
961
  # Get the value of a key.
@@ -711,16 +964,14 @@ class Redis
711
964
  # @return [String]
712
965
  def get(key)
713
966
  synchronize do |client|
714
- client.call [:get, key]
967
+ client.call([:get, key])
715
968
  end
716
969
  end
717
970
 
718
- alias :[] :get
719
-
720
971
  # Get the values of all the given keys.
721
972
  #
722
973
  # @example
723
- # redis.mget("key1", "key1")
974
+ # redis.mget("key1", "key2")
724
975
  # # => ["v1", "v2"]
725
976
  #
726
977
  # @param [Array<String>] keys
@@ -729,14 +980,14 @@ class Redis
729
980
  # @see #mapped_mget
730
981
  def mget(*keys, &blk)
731
982
  synchronize do |client|
732
- client.call [:mget, *keys], &blk
983
+ client.call([:mget, *keys], &blk)
733
984
  end
734
985
  end
735
986
 
736
987
  # Get the values of all the given keys.
737
988
  #
738
989
  # @example
739
- # redis.mapped_mget("key1", "key1")
990
+ # redis.mapped_mget("key1", "key2")
740
991
  # # => { "key1" => "v1", "key2" => "v2" }
741
992
  #
742
993
  # @param [Array<String>] keys array of keys
@@ -745,12 +996,8 @@ class Redis
745
996
  # @see #mget
746
997
  def mapped_mget(*keys)
747
998
  mget(*keys) do |reply|
748
- if reply.kind_of?(Array)
749
- hash = Hash.new
750
- keys.zip(reply).each do |field, value|
751
- hash[field] = value
752
- end
753
- hash
999
+ if reply.is_a?(Array)
1000
+ Hash[keys.zip(reply)]
754
1001
  else
755
1002
  reply
756
1003
  end
@@ -760,48 +1007,48 @@ class Redis
760
1007
  # Overwrite part of a string at key starting at the specified offset.
761
1008
  #
762
1009
  # @param [String] key
763
- # @param [Fixnum] offset byte offset
1010
+ # @param [Integer] offset byte offset
764
1011
  # @param [String] value
765
- # @return [Fixnum] length of the string after it was modified
1012
+ # @return [Integer] length of the string after it was modified
766
1013
  def setrange(key, offset, value)
767
1014
  synchronize do |client|
768
- client.call [:setrange, key, offset, value]
1015
+ client.call([:setrange, key, offset, value.to_s])
769
1016
  end
770
1017
  end
771
1018
 
772
1019
  # Get a substring of the string stored at a key.
773
1020
  #
774
1021
  # @param [String] key
775
- # @param [Fixnum] start zero-based start offset
776
- # @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
777
1024
  # the end of the string
778
- # @return [Fixnum] `0` or `1`
1025
+ # @return [Integer] `0` or `1`
779
1026
  def getrange(key, start, stop)
780
1027
  synchronize do |client|
781
- client.call [:getrange, key, start, stop]
1028
+ client.call([:getrange, key, start, stop])
782
1029
  end
783
1030
  end
784
1031
 
785
1032
  # Sets or clears the bit at offset in the string value stored at key.
786
1033
  #
787
1034
  # @param [String] key
788
- # @param [Fixnum] offset bit offset
789
- # @param [Fixnum] value bit value `0` or `1`
790
- # @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`
791
1038
  def setbit(key, offset, value)
792
1039
  synchronize do |client|
793
- client.call [:setbit, key, offset, value]
1040
+ client.call([:setbit, key, offset, value])
794
1041
  end
795
1042
  end
796
1043
 
797
1044
  # Returns the bit value at offset in the string value stored at key.
798
1045
  #
799
1046
  # @param [String] key
800
- # @param [Fixnum] offset bit offset
801
- # @return [Fixnum] `0` or `1`
1047
+ # @param [Integer] offset bit offset
1048
+ # @return [Integer] `0` or `1`
802
1049
  def getbit(key, offset)
803
1050
  synchronize do |client|
804
- client.call [:getbit, key, offset]
1051
+ client.call([:getbit, key, offset])
805
1052
  end
806
1053
  end
807
1054
 
@@ -809,10 +1056,53 @@ class Redis
809
1056
  #
810
1057
  # @param [String] key
811
1058
  # @param [String] value value to append
812
- # @return [Fixnum] length of the string after appending
1059
+ # @return [Integer] length of the string after appending
813
1060
  def append(key, value)
814
1061
  synchronize do |client|
815
- client.call [:append, key, value]
1062
+ client.call([:append, key, value])
1063
+ end
1064
+ end
1065
+
1066
+ # Count the number of set bits in a range of the string value stored at key.
1067
+ #
1068
+ # @param [String] key
1069
+ # @param [Integer] start start index
1070
+ # @param [Integer] stop stop index
1071
+ # @return [Integer] the number of bits set to 1
1072
+ def bitcount(key, start = 0, stop = -1)
1073
+ synchronize do |client|
1074
+ client.call([:bitcount, key, start, stop])
1075
+ end
1076
+ end
1077
+
1078
+ # Perform a bitwise operation between strings and store the resulting string in a key.
1079
+ #
1080
+ # @param [String] operation e.g. `and`, `or`, `xor`, `not`
1081
+ # @param [String] destkey destination key
1082
+ # @param [String, Array<String>] keys one or more source keys to perform `operation`
1083
+ # @return [Integer] the length of the string stored in `destkey`
1084
+ def bitop(operation, destkey, *keys)
1085
+ synchronize do |client|
1086
+ client.call([:bitop, operation, destkey, *keys])
1087
+ end
1088
+ end
1089
+
1090
+ # Return the position of the first bit set to 1 or 0 in a string.
1091
+ #
1092
+ # @param [String] key
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.
1097
+ # -1 if looking for 1 and it is not found or start and stop are given.
1098
+ def bitpos(key, bit, start = nil, stop = nil)
1099
+ raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
1100
+
1101
+ synchronize do |client|
1102
+ command = [:bitpos, key, bit]
1103
+ command << start if start
1104
+ command << stop if stop
1105
+ client.call(command)
816
1106
  end
817
1107
  end
818
1108
 
@@ -824,39 +1114,131 @@ class Redis
824
1114
  # did not exist
825
1115
  def getset(key, value)
826
1116
  synchronize do |client|
827
- client.call [:getset, key, value]
1117
+ client.call([:getset, key, value.to_s])
1118
+ end
1119
+ end
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)
828
1157
  end
829
1158
  end
830
1159
 
831
1160
  # Get the length of the value stored in a key.
832
1161
  #
833
1162
  # @param [String] key
834
- # @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
835
1164
  # if the key does not exist
836
1165
  def strlen(key)
837
1166
  synchronize do |client|
838
- client.call [:strlen, key]
1167
+ client.call([:strlen, key])
839
1168
  end
840
1169
  end
841
1170
 
842
1171
  # Get the length of a list.
843
1172
  #
844
1173
  # @param [String] key
845
- # @return [Fixnum]
1174
+ # @return [Integer]
846
1175
  def llen(key)
847
1176
  synchronize do |client|
848
- client.call [:llen, key]
1177
+ client.call([:llen, key])
1178
+ end
1179
+ end
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)
849
1231
  end
850
1232
  end
851
1233
 
852
1234
  # Prepend one or more values to a list, creating the list if it doesn't exist
853
1235
  #
854
1236
  # @param [String] key
855
- # @param [String] value
856
- # @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
857
1239
  def lpush(key, value)
858
1240
  synchronize do |client|
859
- client.call [:lpush, key, value]
1241
+ client.call([:lpush, key, value])
860
1242
  end
861
1243
  end
862
1244
 
@@ -864,21 +1246,21 @@ class Redis
864
1246
  #
865
1247
  # @param [String] key
866
1248
  # @param [String] value
867
- # @return [Fixnum] the length of the list after the push operation
1249
+ # @return [Integer] the length of the list after the push operation
868
1250
  def lpushx(key, value)
869
1251
  synchronize do |client|
870
- client.call [:lpushx, key, value]
1252
+ client.call([:lpushx, key, value])
871
1253
  end
872
1254
  end
873
1255
 
874
1256
  # Append one or more values to a list, creating the list if it doesn't exist
875
1257
  #
876
1258
  # @param [String] key
877
- # @param [String] value
878
- # @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
879
1261
  def rpush(key, value)
880
1262
  synchronize do |client|
881
- client.call [:rpush, key, value]
1263
+ client.call([:rpush, key, value])
882
1264
  end
883
1265
  end
884
1266
 
@@ -886,30 +1268,36 @@ class Redis
886
1268
  #
887
1269
  # @param [String] key
888
1270
  # @param [String] value
889
- # @return [Fixnum] the length of the list after the push operation
1271
+ # @return [Integer] the length of the list after the push operation
890
1272
  def rpushx(key, value)
891
1273
  synchronize do |client|
892
- client.call [:rpushx, key, value]
1274
+ client.call([:rpushx, key, value])
893
1275
  end
894
1276
  end
895
1277
 
896
- # Remove and get the first element in a list.
1278
+ # Remove and get the first elements in a list.
897
1279
  #
898
1280
  # @param [String] key
899
- # @return [String]
900
- 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)
901
1284
  synchronize do |client|
902
- client.call [:lpop, key]
1285
+ command = [:lpop, key]
1286
+ command << count if count
1287
+ client.call(command)
903
1288
  end
904
1289
  end
905
1290
 
906
- # Remove and get the last element in a list.
1291
+ # Remove and get the last elements in a list.
907
1292
  #
908
1293
  # @param [String] key
909
- # @return [String]
910
- 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)
911
1297
  synchronize do |client|
912
- client.call [:rpop, key]
1298
+ command = [:rpop, key]
1299
+ command << count if count
1300
+ client.call(command)
913
1301
  end
914
1302
  end
915
1303
 
@@ -920,30 +1308,31 @@ class Redis
920
1308
  # @return [nil, String] the element, or nil when the source key does not exist
921
1309
  def rpoplpush(source, destination)
922
1310
  synchronize do |client|
923
- client.call [:rpoplpush, source, destination]
1311
+ client.call([:rpoplpush, source, destination])
924
1312
  end
925
1313
  end
926
1314
 
927
- def _bpop(cmd, args)
928
- options = {}
929
-
930
- case args.last
931
- when Hash
1315
+ def _bpop(cmd, args, &blk)
1316
+ timeout = if args.last.is_a?(Hash)
932
1317
  options = args.pop
933
- when Integer
1318
+ options[:timeout]
1319
+ elsif args.last.respond_to?(:to_int)
934
1320
  # Issue deprecation notice in obnoxious mode...
935
- options[:timeout] = args.pop
1321
+ args.pop.to_int
936
1322
  end
937
1323
 
1324
+ timeout ||= 0
1325
+
938
1326
  if args.size > 1
939
1327
  # Issue deprecation notice in obnoxious mode...
940
1328
  end
941
1329
 
942
1330
  keys = args.flatten
943
- timeout = options[:timeout] || 0
944
1331
 
945
1332
  synchronize do |client|
946
- client.call_without_timeout [cmd, keys, timeout]
1333
+ command = [cmd, keys, timeout]
1334
+ timeout += client.timeout if timeout > 0
1335
+ client.call_with_timeout(command, timeout, &blk)
947
1336
  end
948
1337
  end
949
1338
 
@@ -963,7 +1352,7 @@ class Redis
963
1352
  # @param [String, Array<String>] keys one or more keys to perform the
964
1353
  # blocking pop on
965
1354
  # @param [Hash] options
966
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1355
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
967
1356
  #
968
1357
  # @return [nil, [String, String]]
969
1358
  # - `nil` when the operation timed out
@@ -977,7 +1366,7 @@ class Redis
977
1366
  # @param [String, Array<String>] keys one or more keys to perform the
978
1367
  # blocking pop on
979
1368
  # @param [Hash] options
980
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1369
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
981
1370
  #
982
1371
  # @return [nil, [String, String]]
983
1372
  # - `nil` when the operation timed out
@@ -994,33 +1383,27 @@ class Redis
994
1383
  # @param [String] source source key
995
1384
  # @param [String] destination destination key
996
1385
  # @param [Hash] options
997
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1386
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
998
1387
  #
999
1388
  # @return [nil, String]
1000
1389
  # - `nil` when the operation timed out
1001
1390
  # - the element was popped and pushed otherwise
1002
- def brpoplpush(source, destination, options = {})
1003
- case options
1004
- when Integer
1005
- # Issue deprecation notice in obnoxious mode...
1006
- options = { :timeout => options }
1007
- end
1008
-
1009
- timeout = options[:timeout] || 0
1010
-
1391
+ def brpoplpush(source, destination, deprecated_timeout = 0, timeout: deprecated_timeout)
1011
1392
  synchronize do |client|
1012
- client.call_without_timeout [:brpoplpush, source, destination, timeout]
1393
+ command = [:brpoplpush, source, destination, timeout]
1394
+ timeout += client.timeout if timeout > 0
1395
+ client.call_with_timeout(command, timeout)
1013
1396
  end
1014
1397
  end
1015
1398
 
1016
1399
  # Get an element from a list by its index.
1017
1400
  #
1018
1401
  # @param [String] key
1019
- # @param [Fixnum] index
1402
+ # @param [Integer] index
1020
1403
  # @return [String]
1021
1404
  def lindex(key, index)
1022
1405
  synchronize do |client|
1023
- client.call [:lindex, key, index]
1406
+ client.call([:lindex, key, index])
1024
1407
  end
1025
1408
  end
1026
1409
 
@@ -1030,72 +1413,72 @@ class Redis
1030
1413
  # @param [String, Symbol] where `BEFORE` or `AFTER`
1031
1414
  # @param [String] pivot reference element
1032
1415
  # @param [String] value
1033
- # @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`
1034
1417
  # when the element `pivot` was not found
1035
1418
  def linsert(key, where, pivot, value)
1036
1419
  synchronize do |client|
1037
- client.call [:linsert, key, where, pivot, value]
1420
+ client.call([:linsert, key, where, pivot, value])
1038
1421
  end
1039
1422
  end
1040
1423
 
1041
1424
  # Get a range of elements from a list.
1042
1425
  #
1043
1426
  # @param [String] key
1044
- # @param [Fixnum] start start index
1045
- # @param [Fixnum] stop stop index
1427
+ # @param [Integer] start start index
1428
+ # @param [Integer] stop stop index
1046
1429
  # @return [Array<String>]
1047
1430
  def lrange(key, start, stop)
1048
1431
  synchronize do |client|
1049
- client.call [:lrange, key, start, stop]
1432
+ client.call([:lrange, key, start, stop])
1050
1433
  end
1051
1434
  end
1052
1435
 
1053
1436
  # Remove elements from a list.
1054
1437
  #
1055
1438
  # @param [String] key
1056
- # @param [Fixnum] count number of elements to remove. Use a positive
1439
+ # @param [Integer] count number of elements to remove. Use a positive
1057
1440
  # value to remove the first `count` occurrences of `value`. A negative
1058
1441
  # value to remove the last `count` occurrences of `value`. Or zero, to
1059
1442
  # remove all occurrences of `value` from the list.
1060
1443
  # @param [String] value
1061
- # @return [Fixnum] the number of removed elements
1444
+ # @return [Integer] the number of removed elements
1062
1445
  def lrem(key, count, value)
1063
1446
  synchronize do |client|
1064
- client.call [:lrem, key, count, value]
1447
+ client.call([:lrem, key, count, value])
1065
1448
  end
1066
1449
  end
1067
1450
 
1068
1451
  # Set the value of an element in a list by its index.
1069
1452
  #
1070
1453
  # @param [String] key
1071
- # @param [Fixnum] index
1454
+ # @param [Integer] index
1072
1455
  # @param [String] value
1073
1456
  # @return [String] `OK`
1074
1457
  def lset(key, index, value)
1075
1458
  synchronize do |client|
1076
- client.call [:lset, key, index, value]
1459
+ client.call([:lset, key, index, value])
1077
1460
  end
1078
1461
  end
1079
1462
 
1080
1463
  # Trim a list to the specified range.
1081
1464
  #
1082
1465
  # @param [String] key
1083
- # @param [Fixnum] start start index
1084
- # @param [Fixnum] stop stop index
1466
+ # @param [Integer] start start index
1467
+ # @param [Integer] stop stop index
1085
1468
  # @return [String] `OK`
1086
1469
  def ltrim(key, start, stop)
1087
1470
  synchronize do |client|
1088
- client.call [:ltrim, key, start, stop]
1471
+ client.call([:ltrim, key, start, stop])
1089
1472
  end
1090
1473
  end
1091
1474
 
1092
1475
  # Get the number of members in a set.
1093
1476
  #
1094
1477
  # @param [String] key
1095
- # @return [Fixnum]
1478
+ # @return [Integer]
1096
1479
  def scard(key)
1097
1480
  synchronize do |client|
1098
- client.call [:scard, key]
1481
+ client.call([:scard, key])
1099
1482
  end
1100
1483
  end
1101
1484
 
@@ -1103,19 +1486,19 @@ class Redis
1103
1486
  #
1104
1487
  # @param [String] key
1105
1488
  # @param [String, Array<String>] member one member, or array of members
1106
- # @return [Boolean, Fixnum] `Boolean` when a single member is specified,
1107
- # 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
1108
1491
  # array of members is specified, holding the number of members that were
1109
1492
  # successfully added
1110
1493
  def sadd(key, member)
1111
1494
  synchronize do |client|
1112
- client.call [:sadd, key, member] do |reply|
1495
+ client.call([:sadd, key, member]) do |reply|
1113
1496
  if member.is_a? Array
1114
1497
  # Variadic: return integer
1115
1498
  reply
1116
1499
  else
1117
1500
  # Single argument: return boolean
1118
- _boolify.call(reply)
1501
+ Boolify.call(reply)
1119
1502
  end
1120
1503
  end
1121
1504
  end
@@ -1125,41 +1508,51 @@ class Redis
1125
1508
  #
1126
1509
  # @param [String] key
1127
1510
  # @param [String, Array<String>] member one member, or array of members
1128
- # @return [Boolean, Fixnum] `Boolean` when a single member is specified,
1129
- # 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
1130
1513
  # array of members is specified, holding the number of members that were
1131
1514
  # successfully removed
1132
1515
  def srem(key, member)
1133
1516
  synchronize do |client|
1134
- client.call [:srem, key, member] do |reply|
1517
+ client.call([:srem, key, member]) do |reply|
1135
1518
  if member.is_a? Array
1136
1519
  # Variadic: return integer
1137
1520
  reply
1138
1521
  else
1139
1522
  # Single argument: return boolean
1140
- _boolify.call(reply)
1523
+ Boolify.call(reply)
1141
1524
  end
1142
1525
  end
1143
1526
  end
1144
1527
  end
1145
1528
 
1146
- # Remove and return a random member from a set.
1529
+ # Remove and return one or more random member from a set.
1147
1530
  #
1148
1531
  # @param [String] key
1149
1532
  # @return [String]
1150
- def spop(key)
1533
+ # @param [Integer] count
1534
+ def spop(key, count = nil)
1151
1535
  synchronize do |client|
1152
- client.call [:spop, key]
1536
+ if count.nil?
1537
+ client.call([:spop, key])
1538
+ else
1539
+ client.call([:spop, key, count])
1540
+ end
1153
1541
  end
1154
1542
  end
1155
1543
 
1156
- # Get a random member from a set.
1544
+ # Get one or more random members from a set.
1157
1545
  #
1158
1546
  # @param [String] key
1547
+ # @param [Integer] count
1159
1548
  # @return [String]
1160
- def srandmember(key)
1549
+ def srandmember(key, count = nil)
1161
1550
  synchronize do |client|
1162
- client.call [:srandmember, key]
1551
+ if count.nil?
1552
+ client.call([:srandmember, key])
1553
+ else
1554
+ client.call([:srandmember, key, count])
1555
+ end
1163
1556
  end
1164
1557
  end
1165
1558
 
@@ -1171,7 +1564,7 @@ class Redis
1171
1564
  # @return [Boolean]
1172
1565
  def smove(source, destination, member)
1173
1566
  synchronize do |client|
1174
- client.call [:smove, source, destination, member], &_boolify
1567
+ client.call([:smove, source, destination, member], &Boolify)
1175
1568
  end
1176
1569
  end
1177
1570
 
@@ -1182,7 +1575,20 @@ class Redis
1182
1575
  # @return [Boolean]
1183
1576
  def sismember(key, member)
1184
1577
  synchronize do |client|
1185
- client.call [:sismember, key, member], &_boolify
1578
+ client.call([:sismember, key, member], &Boolify)
1579
+ end
1580
+ end
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
1186
1592
  end
1187
1593
  end
1188
1594
 
@@ -1192,7 +1598,7 @@ class Redis
1192
1598
  # @return [Array<String>]
1193
1599
  def smembers(key)
1194
1600
  synchronize do |client|
1195
- client.call [:smembers, key]
1601
+ client.call([:smembers, key])
1196
1602
  end
1197
1603
  end
1198
1604
 
@@ -1202,7 +1608,7 @@ class Redis
1202
1608
  # @return [Array<String>] members in the difference
1203
1609
  def sdiff(*keys)
1204
1610
  synchronize do |client|
1205
- client.call [:sdiff, *keys]
1611
+ client.call([:sdiff, *keys])
1206
1612
  end
1207
1613
  end
1208
1614
 
@@ -1210,10 +1616,10 @@ class Redis
1210
1616
  #
1211
1617
  # @param [String] destination destination key
1212
1618
  # @param [String, Array<String>] keys keys pointing to sets to subtract
1213
- # @return [Fixnum] number of elements in the resulting set
1619
+ # @return [Integer] number of elements in the resulting set
1214
1620
  def sdiffstore(destination, *keys)
1215
1621
  synchronize do |client|
1216
- client.call [:sdiffstore, destination, *keys]
1622
+ client.call([:sdiffstore, destination, *keys])
1217
1623
  end
1218
1624
  end
1219
1625
 
@@ -1223,7 +1629,7 @@ class Redis
1223
1629
  # @return [Array<String>] members in the intersection
1224
1630
  def sinter(*keys)
1225
1631
  synchronize do |client|
1226
- client.call [:sinter, *keys]
1632
+ client.call([:sinter, *keys])
1227
1633
  end
1228
1634
  end
1229
1635
 
@@ -1231,10 +1637,10 @@ class Redis
1231
1637
  #
1232
1638
  # @param [String] destination destination key
1233
1639
  # @param [String, Array<String>] keys keys pointing to sets to intersect
1234
- # @return [Fixnum] number of elements in the resulting set
1640
+ # @return [Integer] number of elements in the resulting set
1235
1641
  def sinterstore(destination, *keys)
1236
1642
  synchronize do |client|
1237
- client.call [:sinterstore, destination, *keys]
1643
+ client.call([:sinterstore, destination, *keys])
1238
1644
  end
1239
1645
  end
1240
1646
 
@@ -1244,7 +1650,7 @@ class Redis
1244
1650
  # @return [Array<String>] members in the union
1245
1651
  def sunion(*keys)
1246
1652
  synchronize do |client|
1247
- client.call [:sunion, *keys]
1653
+ client.call([:sunion, *keys])
1248
1654
  end
1249
1655
  end
1250
1656
 
@@ -1252,10 +1658,10 @@ class Redis
1252
1658
  #
1253
1659
  # @param [String] destination destination key
1254
1660
  # @param [String, Array<String>] keys keys pointing to sets to unify
1255
- # @return [Fixnum] number of elements in the resulting set
1661
+ # @return [Integer] number of elements in the resulting set
1256
1662
  def sunionstore(destination, *keys)
1257
1663
  synchronize do |client|
1258
- client.call [:sunionstore, destination, *keys]
1664
+ client.call([:sunionstore, destination, *keys])
1259
1665
  end
1260
1666
  end
1261
1667
 
@@ -1266,10 +1672,10 @@ class Redis
1266
1672
  # # => 4
1267
1673
  #
1268
1674
  # @param [String] key
1269
- # @return [Fixnum]
1675
+ # @return [Integer]
1270
1676
  def zcard(key)
1271
1677
  synchronize do |client|
1272
- client.call [:zcard, key]
1678
+ client.call([:zcard, key])
1273
1679
  end
1274
1680
  end
1275
1681
 
@@ -1285,20 +1691,45 @@ class Redis
1285
1691
  # @param [[Float, String], Array<[Float, String]>] args
1286
1692
  # - a single `[score, member]` pair
1287
1693
  # - an array of `[score, member]` pairs
1288
- #
1289
- # @return [Boolean, Fixnum]
1694
+ # @param [Hash] options
1695
+ # - `:xx => true`: Only update elements that already exist (never
1696
+ # add elements)
1697
+ # - `:nx => true`: Don't update already existing elements (always
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
1703
+ # - `:ch => true`: Modify the return value from the number of new
1704
+ # elements added, to the total number of elements changed (CH is an
1705
+ # abbreviation of changed); changed elements are new elements added
1706
+ # and elements already existing for which the score was updated
1707
+ # - `:incr => true`: When this option is specified ZADD acts like
1708
+ # ZINCRBY; only one score-element pair can be specified in this mode
1709
+ #
1710
+ # @return [Boolean, Integer, Float]
1290
1711
  # - `Boolean` when a single pair is specified, holding whether or not it was
1291
- # **added** to the sorted set
1292
- # - `Fixnum` when an array of pairs is specified, holding the number of
1293
- # pairs that were **added** to the sorted set
1294
- def zadd(key, *args)
1712
+ # **added** to the sorted set.
1713
+ # - `Integer` when an array of pairs is specified, holding the number of
1714
+ # pairs that were **added** to the sorted set.
1715
+ # - `Float` when option :incr is specified, holding the score of the member
1716
+ # after incrementing it.
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
1725
+
1295
1726
  synchronize do |client|
1296
1727
  if args.size == 1 && args[0].is_a?(Array)
1297
- # Variadic: return integer
1298
- client.call [:zadd, key] + args[0]
1728
+ # Variadic: return float if INCR, integer if !INCR
1729
+ client.call(command + args[0], &(incr ? Floatify : nil))
1299
1730
  elsif args.size == 2
1300
- # Single pair: return boolean
1301
- client.call [:zadd, key, args[0], args[1]], &_boolify
1731
+ # Single pair: return float if INCR, boolean if !INCR
1732
+ client.call(command + args, &(incr ? Floatify : Boolify))
1302
1733
  else
1303
1734
  raise ArgumentError, "wrong number of arguments"
1304
1735
  end
@@ -1317,9 +1748,7 @@ class Redis
1317
1748
  # @return [Float] score of the member after incrementing it
1318
1749
  def zincrby(key, increment, member)
1319
1750
  synchronize do |client|
1320
- client.call [:zincrby, key, increment, member] do |reply|
1321
- Float(reply) if reply
1322
- end
1751
+ client.call([:zincrby, key, increment, member], &Floatify)
1323
1752
  end
1324
1753
  end
1325
1754
 
@@ -1335,25 +1764,109 @@ class Redis
1335
1764
  # - a single member
1336
1765
  # - an array of members
1337
1766
  #
1338
- # @return [Boolean, Fixnum]
1767
+ # @return [Boolean, Integer]
1339
1768
  # - `Boolean` when a single member is specified, holding whether or not it
1340
1769
  # was removed from the sorted set
1341
- # - `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
1342
1771
  # members that were removed to the sorted set
1343
1772
  def zrem(key, member)
1344
1773
  synchronize do |client|
1345
- client.call [:zrem, key, member] do |reply|
1774
+ client.call([:zrem, key, member]) do |reply|
1346
1775
  if member.is_a? Array
1347
1776
  # Variadic: return integer
1348
1777
  reply
1349
1778
  else
1350
1779
  # Single argument: return boolean
1351
- _boolify.call(reply)
1780
+ Boolify.call(reply)
1352
1781
  end
1353
1782
  end
1354
1783
  end
1355
1784
  end
1356
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
+
1357
1870
  # Get the score associated with the given member in a sorted set.
1358
1871
  #
1359
1872
  # @example Get the score for member "a"
@@ -1365,12 +1878,67 @@ class Redis
1365
1878
  # @return [Float] score of the member
1366
1879
  def zscore(key, member)
1367
1880
  synchronize do |client|
1368
- client.call [:zscore, key, member] do |reply|
1369
- Float(reply) if reply
1881
+ client.call([:zscore, key, member], &Floatify)
1882
+ end
1883
+ end
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)
1370
1898
  end
1371
1899
  end
1372
1900
  end
1373
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
+
1374
1942
  # Return a range of members in a sorted set, by index.
1375
1943
  #
1376
1944
  # @example Retrieve all members from a sorted set
@@ -1381,32 +1949,24 @@ class Redis
1381
1949
  # # => [["a", 32.0], ["b", 64.0]]
1382
1950
  #
1383
1951
  # @param [String] key
1384
- # @param [Fixnum] start start index
1385
- # @param [Fixnum] stop stop index
1952
+ # @param [Integer] start start index
1953
+ # @param [Integer] stop stop index
1386
1954
  # @param [Hash] options
1387
1955
  # - `:with_scores => true`: include scores in output
1388
1956
  #
1389
1957
  # @return [Array<String>, Array<[String, Float]>]
1390
1958
  # - when `:with_scores` is not specified, an array of members
1391
1959
  # - when `:with_scores` is specified, an array with `[member, score]` pairs
1392
- def zrange(key, start, stop, options = {})
1393
- args = []
1960
+ def zrange(key, start, stop, withscores: false, with_scores: withscores)
1961
+ args = [:zrange, key, start, stop]
1394
1962
 
1395
- with_scores = options[:with_scores] || options[:withscores]
1396
- args << "WITHSCORES" if with_scores
1963
+ if with_scores
1964
+ args << "WITHSCORES"
1965
+ block = FloatifyPairs
1966
+ end
1397
1967
 
1398
1968
  synchronize do |client|
1399
- client.call [:zrange, key, start, stop, *args] do |reply|
1400
- if with_scores
1401
- if reply
1402
- reply.each_slice(2).map do |member, score|
1403
- [member, Float(score)]
1404
- end
1405
- end
1406
- else
1407
- reply
1408
- end
1409
- end
1969
+ client.call(args, &block)
1410
1970
  end
1411
1971
  end
1412
1972
 
@@ -1421,24 +1981,16 @@ class Redis
1421
1981
  # # => [["b", 64.0], ["a", 32.0]]
1422
1982
  #
1423
1983
  # @see #zrange
1424
- def zrevrange(key, start, stop, options = {})
1425
- args = []
1984
+ def zrevrange(key, start, stop, withscores: false, with_scores: withscores)
1985
+ args = [:zrevrange, key, start, stop]
1426
1986
 
1427
- with_scores = options[:with_scores] || options[:withscores]
1428
- args << "WITHSCORES" if with_scores
1987
+ if with_scores
1988
+ args << "WITHSCORES"
1989
+ block = FloatifyPairs
1990
+ end
1429
1991
 
1430
1992
  synchronize do |client|
1431
- client.call [:zrevrange, key, start, stop, *args] do |reply|
1432
- if with_scores
1433
- if reply
1434
- reply.each_slice(2).map do |member, score|
1435
- [member, Float(score)]
1436
- end
1437
- end
1438
- else
1439
- reply
1440
- end
1441
- end
1993
+ client.call(args, &block)
1442
1994
  end
1443
1995
  end
1444
1996
 
@@ -1446,10 +1998,10 @@ class Redis
1446
1998
  #
1447
1999
  # @param [String] key
1448
2000
  # @param [String] member
1449
- # @return [Fixnum]
2001
+ # @return [Integer]
1450
2002
  def zrank(key, member)
1451
2003
  synchronize do |client|
1452
- client.call [:zrank, key, member]
2004
+ client.call([:zrank, key, member])
1453
2005
  end
1454
2006
  end
1455
2007
 
@@ -1458,10 +2010,10 @@ class Redis
1458
2010
  #
1459
2011
  # @param [String] key
1460
2012
  # @param [String] member
1461
- # @return [Fixnum]
2013
+ # @return [Integer]
1462
2014
  def zrevrank(key, member)
1463
2015
  synchronize do |client|
1464
- client.call [:zrevrank, key, member]
2016
+ client.call([:zrevrank, key, member])
1465
2017
  end
1466
2018
  end
1467
2019
 
@@ -1475,12 +2027,94 @@ class Redis
1475
2027
  # # => 5
1476
2028
  #
1477
2029
  # @param [String] key
1478
- # @param [Fixnum] start start index
1479
- # @param [Fixnum] stop stop index
1480
- # @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
1481
2033
  def zremrangebyrank(key, start, stop)
1482
2034
  synchronize do |client|
1483
- client.call [:zremrangebyrank, key, start, stop]
2035
+ client.call([:zremrangebyrank, key, start, stop])
2036
+ end
2037
+ end
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
+
2063
+ # Return a range of members with the same score in a sorted set, by lexicographical ordering
2064
+ #
2065
+ # @example Retrieve members matching a
2066
+ # redis.zrangebylex("zset", "[a", "[a\xff")
2067
+ # # => ["aaren", "aarika", "abagael", "abby"]
2068
+ # @example Retrieve the first 2 members matching a
2069
+ # redis.zrangebylex("zset", "[a", "[a\xff", :limit => [0, 2])
2070
+ # # => ["aaren", "aarika"]
2071
+ #
2072
+ # @param [String] key
2073
+ # @param [String] min
2074
+ # - inclusive minimum is specified by prefixing `(`
2075
+ # - exclusive minimum is specified by prefixing `[`
2076
+ # @param [String] max
2077
+ # - inclusive maximum is specified by prefixing `(`
2078
+ # - exclusive maximum is specified by prefixing `[`
2079
+ # @param [Hash] options
2080
+ # - `:limit => [offset, count]`: skip `offset` members, return a maximum of
2081
+ # `count` members
2082
+ #
2083
+ # @return [Array<String>, Array<[String, Float]>]
2084
+ def zrangebylex(key, min, max, limit: nil)
2085
+ args = [:zrangebylex, key, min, max]
2086
+
2087
+ if limit
2088
+ args << "LIMIT"
2089
+ args.concat(limit)
2090
+ end
2091
+
2092
+ synchronize do |client|
2093
+ client.call(args)
2094
+ end
2095
+ end
2096
+
2097
+ # Return a range of members with the same score in a sorted set, by reversed lexicographical ordering.
2098
+ # Apart from the reversed ordering, #zrevrangebylex is similar to #zrangebylex.
2099
+ #
2100
+ # @example Retrieve members matching a
2101
+ # redis.zrevrangebylex("zset", "[a", "[a\xff")
2102
+ # # => ["abbygail", "abby", "abagael", "aaren"]
2103
+ # @example Retrieve the last 2 members matching a
2104
+ # redis.zrevrangebylex("zset", "[a", "[a\xff", :limit => [0, 2])
2105
+ # # => ["abbygail", "abby"]
2106
+ #
2107
+ # @see #zrangebylex
2108
+ def zrevrangebylex(key, max, min, limit: nil)
2109
+ args = [:zrevrangebylex, key, max, min]
2110
+
2111
+ if limit
2112
+ args << "LIMIT"
2113
+ args.concat(limit)
2114
+ end
2115
+
2116
+ synchronize do |client|
2117
+ client.call(args)
1484
2118
  end
1485
2119
  end
1486
2120
 
@@ -1511,27 +2145,21 @@ class Redis
1511
2145
  # @return [Array<String>, Array<[String, Float]>]
1512
2146
  # - when `:with_scores` is not specified, an array of members
1513
2147
  # - when `:with_scores` is specified, an array with `[member, score]` pairs
1514
- def zrangebyscore(key, min, max, options = {})
1515
- args = []
2148
+ def zrangebyscore(key, min, max, withscores: false, with_scores: withscores, limit: nil)
2149
+ args = [:zrangebyscore, key, min, max]
1516
2150
 
1517
- with_scores = options[:with_scores] || options[:withscores]
1518
- args.concat ["WITHSCORES"] if with_scores
2151
+ if with_scores
2152
+ args << "WITHSCORES"
2153
+ block = FloatifyPairs
2154
+ end
1519
2155
 
1520
- limit = options[:limit]
1521
- args.concat ["LIMIT", *limit] if limit
2156
+ if limit
2157
+ args << "LIMIT"
2158
+ args.concat(limit)
2159
+ end
1522
2160
 
1523
2161
  synchronize do |client|
1524
- client.call [:zrangebyscore, key, min, max, *args] do |reply|
1525
- if with_scores
1526
- if reply
1527
- reply.each_slice(2).map do |member, score|
1528
- [member, Float(score)]
1529
- end
1530
- end
1531
- else
1532
- reply
1533
- end
1534
- end
2162
+ client.call(args, &block)
1535
2163
  end
1536
2164
  end
1537
2165
 
@@ -1549,27 +2177,21 @@ class Redis
1549
2177
  # # => [["b", 64.0], ["a", 32.0]]
1550
2178
  #
1551
2179
  # @see #zrangebyscore
1552
- def zrevrangebyscore(key, max, min, options = {})
1553
- args = []
2180
+ def zrevrangebyscore(key, max, min, withscores: false, with_scores: withscores, limit: nil)
2181
+ args = [:zrevrangebyscore, key, max, min]
1554
2182
 
1555
- with_scores = options[:with_scores] || options[:withscores]
1556
- args.concat ["WITHSCORES"] if with_scores
2183
+ if with_scores
2184
+ args << "WITHSCORES"
2185
+ block = FloatifyPairs
2186
+ end
1557
2187
 
1558
- limit = options[:limit]
1559
- args.concat ["LIMIT", *limit] if limit
2188
+ if limit
2189
+ args << "LIMIT"
2190
+ args.concat(limit)
2191
+ end
1560
2192
 
1561
2193
  synchronize do |client|
1562
- client.call [:zrevrangebyscore, key, max, min, *args] do |reply|
1563
- if with_scores
1564
- if reply
1565
- reply.each_slice(2).map do |member, score|
1566
- [member, Float(score)]
1567
- end
1568
- end
1569
- else
1570
- reply
1571
- end
1572
- end
2194
+ client.call(args, &block)
1573
2195
  end
1574
2196
  end
1575
2197
 
@@ -1589,10 +2211,10 @@ class Redis
1589
2211
  # @param [String] max
1590
2212
  # - inclusive maximum score is specified verbatim
1591
2213
  # - exclusive maximum score is specified by prefixing `(`
1592
- # @return [Fixnum] number of members that were removed
2214
+ # @return [Integer] number of members that were removed
1593
2215
  def zremrangebyscore(key, min, max)
1594
2216
  synchronize do |client|
1595
- client.call [:zremrangebyscore, key, min, max]
2217
+ client.call([:zremrangebyscore, key, min, max])
1596
2218
  end
1597
2219
  end
1598
2220
 
@@ -1612,10 +2234,49 @@ class Redis
1612
2234
  # @param [String] max
1613
2235
  # - inclusive maximum score is specified verbatim
1614
2236
  # - exclusive maximum score is specified by prefixing `(`
1615
- # @return [Fixnum] number of members in within the specified range
2237
+ # @return [Integer] number of members in within the specified range
1616
2238
  def zcount(key, min, max)
1617
2239
  synchronize do |client|
1618
- client.call [:zcount, key, min, max]
2240
+ client.call([:zcount, key, min, max])
2241
+ end
2242
+ end
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)
1619
2280
  end
1620
2281
  end
1621
2282
 
@@ -1632,18 +2293,19 @@ class Redis
1632
2293
  # - `:weights => [Float, Float, ...]`: weights to associate with source
1633
2294
  # sorted sets
1634
2295
  # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
1635
- # @return [Fixnum] number of elements in the resulting sorted set
1636
- def zinterstore(destination, keys, options = {})
1637
- 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]
1638
2299
 
1639
- weights = options[:weights]
1640
- args.concat ["WEIGHTS", *weights] if weights
2300
+ if weights
2301
+ args << "WEIGHTS"
2302
+ args.concat(weights)
2303
+ end
1641
2304
 
1642
- aggregate = options[:aggregate]
1643
- args.concat ["AGGREGATE", aggregate] if aggregate
2305
+ args << "AGGREGATE" << aggregate if aggregate
1644
2306
 
1645
2307
  synchronize do |client|
1646
- client.call [:zinterstore, destination, keys.size, *(keys + args)]
2308
+ client.call(args)
1647
2309
  end
1648
2310
  end
1649
2311
 
@@ -1659,40 +2321,46 @@ class Redis
1659
2321
  # - `:weights => [Float, Float, ...]`: weights to associate with source
1660
2322
  # sorted sets
1661
2323
  # - `:aggregate => String`: aggregate function to use (sum, min, max, ...)
1662
- # @return [Fixnum] number of elements in the resulting sorted set
1663
- def zunionstore(destination, keys, options = {})
1664
- 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]
1665
2327
 
1666
- weights = options[:weights]
1667
- args.concat ["WEIGHTS", *weights] if weights
2328
+ if weights
2329
+ args << "WEIGHTS"
2330
+ args.concat(weights)
2331
+ end
1668
2332
 
1669
- aggregate = options[:aggregate]
1670
- args.concat ["AGGREGATE", aggregate] if aggregate
2333
+ args << "AGGREGATE" << aggregate if aggregate
1671
2334
 
1672
2335
  synchronize do |client|
1673
- client.call [:zunionstore, destination, keys.size, *(keys + args)]
2336
+ client.call(args)
1674
2337
  end
1675
2338
  end
1676
2339
 
1677
2340
  # Get the number of fields in a hash.
1678
2341
  #
1679
2342
  # @param [String] key
1680
- # @return [Fixnum] number of fields in the hash
2343
+ # @return [Integer] number of fields in the hash
1681
2344
  def hlen(key)
1682
2345
  synchronize do |client|
1683
- client.call [:hlen, key]
2346
+ client.call([:hlen, key])
1684
2347
  end
1685
2348
  end
1686
2349
 
1687
- # 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
1688
2355
  #
1689
2356
  # @param [String] key
1690
- # @param [String] field
1691
- # @param [String] value
1692
- # @return [Boolean] whether or not the field was **added** to the hash
1693
- 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
+
1694
2362
  synchronize do |client|
1695
- client.call [:hset, key, field, value], &_boolify
2363
+ client.call([:hset, key, *attrs])
1696
2364
  end
1697
2365
  end
1698
2366
 
@@ -1704,7 +2372,7 @@ class Redis
1704
2372
  # @return [Boolean] whether or not the field was **added** to the hash
1705
2373
  def hsetnx(key, field, value)
1706
2374
  synchronize do |client|
1707
- client.call [:hsetnx, key, field, value], &_boolify
2375
+ client.call([:hsetnx, key, field, value], &Boolify)
1708
2376
  end
1709
2377
  end
1710
2378
 
@@ -1716,28 +2384,28 @@ class Redis
1716
2384
  #
1717
2385
  # @param [String] key
1718
2386
  # @param [Array<String>] attrs array of fields and values
1719
- # @return `"OK"`
2387
+ # @return [String] `"OK"`
1720
2388
  #
1721
2389
  # @see #mapped_hmset
1722
2390
  def hmset(key, *attrs)
1723
2391
  synchronize do |client|
1724
- client.call [:hmset, key, *attrs]
2392
+ client.call([:hmset, key] + attrs)
1725
2393
  end
1726
2394
  end
1727
2395
 
1728
2396
  # Set one or more hash values.
1729
2397
  #
1730
2398
  # @example
1731
- # redis.hmset("hash", { "f1" => "v1", "f2" => "v2" })
2399
+ # redis.mapped_hmset("hash", { "f1" => "v1", "f2" => "v2" })
1732
2400
  # # => "OK"
1733
2401
  #
1734
2402
  # @param [String] key
1735
- # @param [Hash] hash fields mapping to values
1736
- # @return `"OK"`
2403
+ # @param [Hash] hash a non-empty hash with fields mapping to values
2404
+ # @return [String] `"OK"`
1737
2405
  #
1738
2406
  # @see #hmset
1739
2407
  def mapped_hmset(key, hash)
1740
- hmset(key, *hash.to_a.flatten)
2408
+ hmset(key, hash.to_a.flatten)
1741
2409
  end
1742
2410
 
1743
2411
  # Get the value of a hash field.
@@ -1747,7 +2415,7 @@ class Redis
1747
2415
  # @return [String]
1748
2416
  def hget(key, field)
1749
2417
  synchronize do |client|
1750
- client.call [:hget, key, field]
2418
+ client.call([:hget, key, field])
1751
2419
  end
1752
2420
  end
1753
2421
 
@@ -1764,14 +2432,14 @@ class Redis
1764
2432
  # @see #mapped_hmget
1765
2433
  def hmget(key, *fields, &blk)
1766
2434
  synchronize do |client|
1767
- client.call [:hmget, key, *fields], &blk
2435
+ client.call([:hmget, key] + fields, &blk)
1768
2436
  end
1769
2437
  end
1770
2438
 
1771
2439
  # Get the values of all the given hash fields.
1772
2440
  #
1773
2441
  # @example
1774
- # redis.hmget("hash", "f1", "f2")
2442
+ # redis.mapped_hmget("hash", "f1", "f2")
1775
2443
  # # => { "f1" => "v1", "f2" => "v2" }
1776
2444
  #
1777
2445
  # @param [String] key
@@ -1781,12 +2449,8 @@ class Redis
1781
2449
  # @see #hmget
1782
2450
  def mapped_hmget(key, *fields)
1783
2451
  hmget(key, *fields) do |reply|
1784
- if reply.kind_of?(Array)
1785
- hash = Hash.new
1786
- fields.zip(reply).each do |field, value|
1787
- hash[field] = value
1788
- end
1789
- hash
2452
+ if reply.is_a?(Array)
2453
+ Hash[fields.zip(reply)]
1790
2454
  else
1791
2455
  reply
1792
2456
  end
@@ -1797,10 +2461,10 @@ class Redis
1797
2461
  #
1798
2462
  # @param [String] key
1799
2463
  # @param [String, Array<String>] field
1800
- # @return [Fixnum] the number of fields that were removed from the hash
1801
- def hdel(key, field)
2464
+ # @return [Integer] the number of fields that were removed from the hash
2465
+ def hdel(key, *fields)
1802
2466
  synchronize do |client|
1803
- client.call [:hdel, key, field]
2467
+ client.call([:hdel, key, *fields])
1804
2468
  end
1805
2469
  end
1806
2470
 
@@ -1811,7 +2475,7 @@ class Redis
1811
2475
  # @return [Boolean] whether or not the field exists in the hash
1812
2476
  def hexists(key, field)
1813
2477
  synchronize do |client|
1814
- client.call [:hexists, key, field], &_boolify
2478
+ client.call([:hexists, key, field], &Boolify)
1815
2479
  end
1816
2480
  end
1817
2481
 
@@ -1819,11 +2483,11 @@ class Redis
1819
2483
  #
1820
2484
  # @param [String] key
1821
2485
  # @param [String] field
1822
- # @param [Fixnum] increment
1823
- # @return [Fixnum] value of the field after incrementing it
2486
+ # @param [Integer] increment
2487
+ # @return [Integer] value of the field after incrementing it
1824
2488
  def hincrby(key, field, increment)
1825
2489
  synchronize do |client|
1826
- client.call [:hincrby, key, field, increment]
2490
+ client.call([:hincrby, key, field, increment])
1827
2491
  end
1828
2492
  end
1829
2493
 
@@ -1835,9 +2499,7 @@ class Redis
1835
2499
  # @return [Float] value of the field after incrementing it
1836
2500
  def hincrbyfloat(key, field, increment)
1837
2501
  synchronize do |client|
1838
- client.call [:hincrbyfloat, key, field, increment] do |reply|
1839
- Float(reply) if reply
1840
- end
2502
+ client.call([:hincrbyfloat, key, field, increment], &Floatify)
1841
2503
  end
1842
2504
  end
1843
2505
 
@@ -1847,7 +2509,7 @@ class Redis
1847
2509
  # @return [Array<String>]
1848
2510
  def hkeys(key)
1849
2511
  synchronize do |client|
1850
- client.call [:hkeys, key]
2512
+ client.call([:hkeys, key])
1851
2513
  end
1852
2514
  end
1853
2515
 
@@ -1857,7 +2519,7 @@ class Redis
1857
2519
  # @return [Array<String>]
1858
2520
  def hvals(key)
1859
2521
  synchronize do |client|
1860
- client.call [:hvals, key]
2522
+ client.call([:hvals, key])
1861
2523
  end
1862
2524
  end
1863
2525
 
@@ -1867,53 +2529,79 @@ class Redis
1867
2529
  # @return [Hash<String, String>]
1868
2530
  def hgetall(key)
1869
2531
  synchronize do |client|
1870
- client.call [:hgetall, key], &_hashify
2532
+ client.call([:hgetall, key], &Hashify)
1871
2533
  end
1872
2534
  end
1873
2535
 
1874
2536
  # Post a message to a channel.
1875
2537
  def publish(channel, message)
1876
2538
  synchronize do |client|
1877
- client.call [:publish, channel, message]
2539
+ client.call([:publish, channel, message])
1878
2540
  end
1879
2541
  end
1880
2542
 
1881
2543
  def subscribed?
1882
2544
  synchronize do |client|
1883
- client.kind_of? SubscribedClient
2545
+ client.is_a? SubscribedClient
1884
2546
  end
1885
2547
  end
1886
2548
 
1887
2549
  # Listen for messages published to the given channels.
1888
2550
  def subscribe(*channels, &block)
1889
- synchronize do |client|
1890
- _subscription(:subscribe, channels, block)
2551
+ synchronize do |_client|
2552
+ _subscription(:subscribe, 0, channels, block)
2553
+ end
2554
+ end
2555
+
2556
+ # Listen for messages published to the given channels. Throw a timeout error
2557
+ # if there is no messages for a timeout period.
2558
+ def subscribe_with_timeout(timeout, *channels, &block)
2559
+ synchronize do |_client|
2560
+ _subscription(:subscribe_with_timeout, timeout, channels, block)
1891
2561
  end
1892
2562
  end
1893
2563
 
1894
2564
  # Stop listening for messages posted to the given channels.
1895
2565
  def unsubscribe(*channels)
1896
2566
  synchronize do |client|
1897
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2567
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2568
+
1898
2569
  client.unsubscribe(*channels)
1899
2570
  end
1900
2571
  end
1901
2572
 
1902
2573
  # Listen for messages published to channels matching the given patterns.
1903
2574
  def psubscribe(*channels, &block)
1904
- synchronize do |client|
1905
- _subscription(:psubscribe, channels, block)
2575
+ synchronize do |_client|
2576
+ _subscription(:psubscribe, 0, channels, block)
2577
+ end
2578
+ end
2579
+
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.
2582
+ def psubscribe_with_timeout(timeout, *channels, &block)
2583
+ synchronize do |_client|
2584
+ _subscription(:psubscribe_with_timeout, timeout, channels, block)
1906
2585
  end
1907
2586
  end
1908
2587
 
1909
2588
  # Stop listening for messages posted to channels matching the given patterns.
1910
2589
  def punsubscribe(*channels)
1911
2590
  synchronize do |client|
1912
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2591
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2592
+
1913
2593
  client.punsubscribe(*channels)
1914
2594
  end
1915
2595
  end
1916
2596
 
2597
+ # Inspect the state of the Pub/Sub subsystem.
2598
+ # Possible subcommands: channels, numsub, numpat.
2599
+ def pubsub(subcommand, *args)
2600
+ synchronize do |client|
2601
+ client.call([:pubsub, subcommand] + args)
2602
+ end
2603
+ end
2604
+
1917
2605
  # Watch the given keys to determine execution of the MULTI/EXEC block.
1918
2606
  #
1919
2607
  # Using a block is optional, but is necessary for thread-safety.
@@ -1946,17 +2634,19 @@ class Redis
1946
2634
  # @see #multi
1947
2635
  def watch(*keys)
1948
2636
  synchronize do |client|
1949
- client.call [:watch, *keys]
2637
+ res = client.call([:watch, *keys])
1950
2638
 
1951
2639
  if block_given?
1952
2640
  begin
1953
- yield
2641
+ yield(self)
1954
2642
  rescue ConnectionError
1955
2643
  raise
1956
2644
  rescue StandardError
1957
2645
  unwatch
1958
2646
  raise
1959
2647
  end
2648
+ else
2649
+ res
1960
2650
  end
1961
2651
  end
1962
2652
  end
@@ -1969,18 +2659,18 @@ class Redis
1969
2659
  # @see #multi
1970
2660
  def unwatch
1971
2661
  synchronize do |client|
1972
- client.call [:unwatch]
2662
+ client.call([:unwatch])
1973
2663
  end
1974
2664
  end
1975
2665
 
1976
2666
  def pipelined
1977
- synchronize do |client|
2667
+ synchronize do |prior_client|
1978
2668
  begin
1979
- original, @client = @client, Pipeline.new
2669
+ @client = Pipeline.new(prior_client)
1980
2670
  yield(self)
1981
- original.call_pipeline(@client)
2671
+ prior_client.call_pipeline(@client)
1982
2672
  ensure
1983
- @client = original
2673
+ @client = prior_client
1984
2674
  end
1985
2675
  end
1986
2676
  end
@@ -2016,17 +2706,16 @@ class Redis
2016
2706
  # @see #watch
2017
2707
  # @see #unwatch
2018
2708
  def multi
2019
- synchronize do |client|
2709
+ synchronize do |prior_client|
2020
2710
  if !block_given?
2021
- client.call [:multi]
2711
+ prior_client.call([:multi])
2022
2712
  else
2023
2713
  begin
2024
- pipeline = Pipeline::Multi.new
2025
- original, @client = @client, pipeline
2714
+ @client = Pipeline::Multi.new(prior_client)
2026
2715
  yield(self)
2027
- original.call_pipeline(pipeline)
2716
+ prior_client.call_pipeline(@client)
2028
2717
  ensure
2029
- @client = original
2718
+ @client = prior_client
2030
2719
  end
2031
2720
  end
2032
2721
  end
@@ -2044,7 +2733,7 @@ class Redis
2044
2733
  # @see #discard
2045
2734
  def exec
2046
2735
  synchronize do |client|
2047
- client.call [:exec]
2736
+ client.call([:exec])
2048
2737
  end
2049
2738
  end
2050
2739
 
@@ -2052,13 +2741,13 @@ class Redis
2052
2741
  #
2053
2742
  # Only call this method when `#multi` was called **without** a block.
2054
2743
  #
2055
- # @return `"OK"`
2744
+ # @return [String] `"OK"`
2056
2745
  #
2057
2746
  # @see #multi
2058
2747
  # @see #exec
2059
2748
  def discard
2060
2749
  synchronize do |client|
2061
- client.call [:discard]
2750
+ client.call([:discard])
2062
2751
  end
2063
2752
  end
2064
2753
 
@@ -2093,8 +2782,8 @@ class Redis
2093
2782
  synchronize do |client|
2094
2783
  arg = args.first
2095
2784
 
2096
- client.call [:script, :exists, arg] do |reply|
2097
- reply = reply.map { |r| _boolify.call(r) }
2785
+ client.call([:script, :exists, arg]) do |reply|
2786
+ reply = reply.map { |r| Boolify.call(r) }
2098
2787
 
2099
2788
  if arg.is_a?(Array)
2100
2789
  reply
@@ -2105,7 +2794,7 @@ class Redis
2105
2794
  end
2106
2795
  else
2107
2796
  synchronize do |client|
2108
- client.call [:script, subcommand] + args
2797
+ client.call([:script, subcommand] + args)
2109
2798
  end
2110
2799
  end
2111
2800
  end
@@ -2119,7 +2808,7 @@ class Redis
2119
2808
  argv = args.shift || options[:argv] || []
2120
2809
 
2121
2810
  synchronize do |client|
2122
- client.call [cmd, script, keys.length] + keys + argv
2811
+ client.call([cmd, script, keys.length] + keys + argv)
2123
2812
  end
2124
2813
  end
2125
2814
 
@@ -2173,60 +2862,975 @@ class Redis
2173
2862
  _eval(:evalsha, args)
2174
2863
  end
2175
2864
 
2176
- def id
2865
+ def _scan(command, cursor, args, match: nil, count: nil, type: nil, &block)
2866
+ # SSCAN/ZSCAN/HSCAN already prepend the key to +args+.
2867
+
2868
+ args << cursor
2869
+ args << "MATCH" << match if match
2870
+ args << "COUNT" << count if count
2871
+ args << "TYPE" << type if type
2872
+
2177
2873
  synchronize do |client|
2178
- client.id
2874
+ client.call([command] + args, &block)
2179
2875
  end
2180
2876
  end
2181
2877
 
2182
- def inspect
2878
+ # Scan the keyspace
2879
+ #
2880
+ # @example Retrieve the first batch of keys
2881
+ # redis.scan(0)
2882
+ # # => ["4", ["key:21", "key:47", "key:42"]]
2883
+ # @example Retrieve a batch of keys matching a pattern
2884
+ # redis.scan(4, :match => "key:1?")
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"]]
2889
+ #
2890
+ # @param [String, Integer] cursor the cursor of the iteration
2891
+ # @param [Hash] options
2892
+ # - `:match => String`: only return keys matching the pattern
2893
+ # - `:count => Integer`: return count keys at most per iteration
2894
+ # - `:type => String`: return keys only of the given type
2895
+ #
2896
+ # @return [String, Array<String>] the next cursor and all found keys
2897
+ def scan(cursor, **options)
2898
+ _scan(:scan, cursor, [], **options)
2899
+ end
2900
+
2901
+ # Scan the keyspace
2902
+ #
2903
+ # @example Retrieve all of the keys (with possible duplicates)
2904
+ # redis.scan_each.to_a
2905
+ # # => ["key:21", "key:47", "key:42"]
2906
+ # @example Execute block for each key matching a pattern
2907
+ # redis.scan_each(:match => "key:1?") {|key| puts key}
2908
+ # # => key:13
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"
2914
+ #
2915
+ # @param [Hash] options
2916
+ # - `:match => String`: only return keys matching the pattern
2917
+ # - `:count => Integer`: return count keys at most per iteration
2918
+ # - `:type => String`: return keys only of the given type
2919
+ #
2920
+ # @return [Enumerator] an enumerator for all found keys
2921
+ def scan_each(**options, &block)
2922
+ return to_enum(:scan_each, **options) unless block_given?
2923
+
2924
+ cursor = 0
2925
+ loop do
2926
+ cursor, keys = scan(cursor, **options)
2927
+ keys.each(&block)
2928
+ break if cursor == "0"
2929
+ end
2930
+ end
2931
+
2932
+ # Scan a hash
2933
+ #
2934
+ # @example Retrieve the first batch of key/value pairs in a hash
2935
+ # redis.hscan("hash", 0)
2936
+ #
2937
+ # @param [String, Integer] cursor the cursor of the iteration
2938
+ # @param [Hash] options
2939
+ # - `:match => String`: only return keys matching the pattern
2940
+ # - `:count => Integer`: return count keys at most per iteration
2941
+ #
2942
+ # @return [String, Array<[String, String]>] the next cursor and all found keys
2943
+ def hscan(key, cursor, **options)
2944
+ _scan(:hscan, cursor, [key], **options) do |reply|
2945
+ [reply[0], reply[1].each_slice(2).to_a]
2946
+ end
2947
+ end
2948
+
2949
+ # Scan a hash
2950
+ #
2951
+ # @example Retrieve all of the key/value pairs in a hash
2952
+ # redis.hscan_each("hash").to_a
2953
+ # # => [["key70", "70"], ["key80", "80"]]
2954
+ #
2955
+ # @param [Hash] options
2956
+ # - `:match => String`: only return keys matching the pattern
2957
+ # - `:count => Integer`: return count keys at most per iteration
2958
+ #
2959
+ # @return [Enumerator] an enumerator for all found keys
2960
+ def hscan_each(key, **options, &block)
2961
+ return to_enum(:hscan_each, key, **options) unless block_given?
2962
+
2963
+ cursor = 0
2964
+ loop do
2965
+ cursor, values = hscan(key, cursor, **options)
2966
+ values.each(&block)
2967
+ break if cursor == "0"
2968
+ end
2969
+ end
2970
+
2971
+ # Scan a sorted set
2972
+ #
2973
+ # @example Retrieve the first batch of key/value pairs in a hash
2974
+ # redis.zscan("zset", 0)
2975
+ #
2976
+ # @param [String, Integer] cursor the cursor of the iteration
2977
+ # @param [Hash] options
2978
+ # - `:match => String`: only return keys matching the pattern
2979
+ # - `:count => Integer`: return count keys at most per iteration
2980
+ #
2981
+ # @return [String, Array<[String, Float]>] the next cursor and all found
2982
+ # members and scores
2983
+ def zscan(key, cursor, **options)
2984
+ _scan(:zscan, cursor, [key], **options) do |reply|
2985
+ [reply[0], FloatifyPairs.call(reply[1])]
2986
+ end
2987
+ end
2988
+
2989
+ # Scan a sorted set
2990
+ #
2991
+ # @example Retrieve all of the members/scores in a sorted set
2992
+ # redis.zscan_each("zset").to_a
2993
+ # # => [["key70", "70"], ["key80", "80"]]
2994
+ #
2995
+ # @param [Hash] options
2996
+ # - `:match => String`: only return keys matching the pattern
2997
+ # - `:count => Integer`: return count keys at most per iteration
2998
+ #
2999
+ # @return [Enumerator] an enumerator for all found scores and members
3000
+ def zscan_each(key, **options, &block)
3001
+ return to_enum(:zscan_each, key, **options) unless block_given?
3002
+
3003
+ cursor = 0
3004
+ loop do
3005
+ cursor, values = zscan(key, cursor, **options)
3006
+ values.each(&block)
3007
+ break if cursor == "0"
3008
+ end
3009
+ end
3010
+
3011
+ # Scan a set
3012
+ #
3013
+ # @example Retrieve the first batch of keys in a set
3014
+ # redis.sscan("set", 0)
3015
+ #
3016
+ # @param [String, Integer] cursor the cursor of the iteration
3017
+ # @param [Hash] options
3018
+ # - `:match => String`: only return keys matching the pattern
3019
+ # - `:count => Integer`: return count keys at most per iteration
3020
+ #
3021
+ # @return [String, Array<String>] the next cursor and all found members
3022
+ def sscan(key, cursor, **options)
3023
+ _scan(:sscan, cursor, [key], **options)
3024
+ end
3025
+
3026
+ # Scan a set
3027
+ #
3028
+ # @example Retrieve all of the keys in a set
3029
+ # redis.sscan_each("set").to_a
3030
+ # # => ["key1", "key2", "key3"]
3031
+ #
3032
+ # @param [Hash] options
3033
+ # - `:match => String`: only return keys matching the pattern
3034
+ # - `:count => Integer`: return count keys at most per iteration
3035
+ #
3036
+ # @return [Enumerator] an enumerator for all keys in the set
3037
+ def sscan_each(key, **options, &block)
3038
+ return to_enum(:sscan_each, key, **options) unless block_given?
3039
+
3040
+ cursor = 0
3041
+ loop do
3042
+ cursor, keys = sscan(key, cursor, **options)
3043
+ keys.each(&block)
3044
+ break if cursor == "0"
3045
+ end
3046
+ end
3047
+
3048
+ # Add one or more members to a HyperLogLog structure.
3049
+ #
3050
+ # @param [String] key
3051
+ # @param [String, Array<String>] member one member, or array of members
3052
+ # @return [Boolean] true if at least 1 HyperLogLog internal register was altered. false otherwise.
3053
+ def pfadd(key, member)
3054
+ synchronize do |client|
3055
+ client.call([:pfadd, key, member], &Boolify)
3056
+ end
3057
+ end
3058
+
3059
+ # Get the approximate cardinality of members added to HyperLogLog structure.
3060
+ #
3061
+ # If called with multiple keys, returns the approximate cardinality of the
3062
+ # union of the HyperLogLogs contained in the keys.
3063
+ #
3064
+ # @param [String, Array<String>] keys
3065
+ # @return [Integer]
3066
+ def pfcount(*keys)
2183
3067
  synchronize do |client|
2184
- "#<Redis client v#{Redis::VERSION} for #{client.id}>"
3068
+ client.call([:pfcount] + keys)
2185
3069
  end
2186
3070
  end
2187
3071
 
2188
- def method_missing(command, *args)
3072
+ # Merge multiple HyperLogLog values into an unique value that will approximate the cardinality of the union of
3073
+ # the observed Sets of the source HyperLogLog structures.
3074
+ #
3075
+ # @param [String] dest_key destination key
3076
+ # @param [String, Array<String>] source_key source key, or array of keys
3077
+ # @return [Boolean]
3078
+ def pfmerge(dest_key, *source_key)
2189
3079
  synchronize do |client|
2190
- client.call [command, *args]
3080
+ client.call([:pfmerge, dest_key, *source_key], &BoolifySet)
2191
3081
  end
2192
3082
  end
2193
3083
 
2194
- private
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
+
3523
+ # Interact with the sentinel command (masters, master, slaves, failover)
3524
+ #
3525
+ # @param [String] subcommand e.g. `masters`, `master`, `slaves`
3526
+ # @param [Array<String>] args depends on subcommand
3527
+ # @return [Array<String>, Hash<String, String>, String] depends on subcommand
3528
+ def sentinel(subcommand, *args)
3529
+ subcommand = subcommand.to_s.downcase
3530
+ synchronize do |client|
3531
+ client.call([:sentinel, subcommand] + args) do |reply|
3532
+ case subcommand
3533
+ when "get-master-addr-by-name"
3534
+ reply
3535
+ else
3536
+ if reply.is_a?(Array)
3537
+ if reply[0].is_a?(Array)
3538
+ reply.map(&Hashify)
3539
+ else
3540
+ Hashify.call(reply)
3541
+ end
3542
+ else
3543
+ reply
3544
+ end
3545
+ end
3546
+ end
3547
+ end
3548
+ end
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
+
3590
+ def id
3591
+ @original_client.id
3592
+ end
3593
+
3594
+ def inspect
3595
+ "#<Redis client v#{Redis::VERSION} for #{id}>"
3596
+ end
3597
+
3598
+ def dup
3599
+ self.class.new(@options)
3600
+ end
3601
+
3602
+ def connection
3603
+ return @original_client.connection_info if @cluster_mode
3604
+
3605
+ {
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
3611
+ }
3612
+ end
3613
+
3614
+ def method_missing(command, *args) # rubocop:disable Style/MissingRespondToMissing
3615
+ synchronize do |client|
3616
+ client.call([command] + args)
3617
+ end
3618
+ end
3619
+
3620
+ private
2195
3621
 
2196
3622
  # Commands returning 1 for true and 0 for false may be executed in a pipeline
2197
3623
  # where the method call will return nil. Propagate the nil instead of falsely
2198
3624
  # returning false.
2199
- def _boolify
2200
- lambda { |value|
2201
- value == 1 if value
3625
+ Boolify = lambda { |value|
3626
+ case value
3627
+ when 1
3628
+ true
3629
+ when 0
3630
+ false
3631
+ else
3632
+ value
3633
+ end
3634
+ }
3635
+
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] }
2202
3705
  }
2203
- end
3706
+ }
2204
3707
 
2205
- def _hashify
2206
- lambda { |array|
2207
- hash = Hash.new
2208
- array.each_slice(2) do |field, value|
2209
- hash[field] = value
2210
- end
2211
- hash
3708
+ HashifyStreamAutoclaimJustId = lambda { |reply|
3709
+ {
3710
+ 'next' => reply[0],
3711
+ 'entries' => reply[1]
3712
+ }
3713
+ }
3714
+
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
2212
3721
  }
3722
+ }
3723
+
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('-'))
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
2213
3779
  end
2214
3780
 
2215
- def _subscription(method, channels, block)
2216
- return @client.call [method, *channels] if subscribed?
3781
+ def _subscription(method, timeout, channels, block)
3782
+ return @client.call([method] + channels) if subscribed?
2217
3783
 
2218
3784
  begin
2219
3785
  original, @client = @client, SubscribedClient.new(@client)
2220
- @client.send(method, *channels, &block)
3786
+ if timeout > 0
3787
+ @client.send(method, timeout, *channels, &block)
3788
+ else
3789
+ @client.send(method, *channels, &block)
3790
+ end
2221
3791
  ensure
2222
3792
  @client = original
2223
3793
  end
2224
3794
  end
2225
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
2226
3829
  end
2227
3830
 
2228
- require "redis/version"
2229
- require "redis/connection"
2230
- require "redis/client"
2231
- require "redis/pipeline"
2232
- 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"