redis 3.0.0 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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"