redis 3.0.0 → 4.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +269 -0
  3. data/README.md +295 -58
  4. data/lib/redis.rb +1760 -451
  5. data/lib/redis/client.rb +355 -88
  6. data/lib/redis/cluster.rb +295 -0
  7. data/lib/redis/cluster/command.rb +81 -0
  8. data/lib/redis/cluster/command_loader.rb +34 -0
  9. data/lib/redis/cluster/key_slot_converter.rb +72 -0
  10. data/lib/redis/cluster/node.rb +107 -0
  11. data/lib/redis/cluster/node_key.rb +31 -0
  12. data/lib/redis/cluster/node_loader.rb +37 -0
  13. data/lib/redis/cluster/option.rb +90 -0
  14. data/lib/redis/cluster/slot.rb +86 -0
  15. data/lib/redis/cluster/slot_loader.rb +49 -0
  16. data/lib/redis/connection.rb +4 -2
  17. data/lib/redis/connection/command_helper.rb +5 -10
  18. data/lib/redis/connection/hiredis.rb +12 -8
  19. data/lib/redis/connection/registry.rb +2 -1
  20. data/lib/redis/connection/ruby.rb +232 -63
  21. data/lib/redis/connection/synchrony.rb +41 -14
  22. data/lib/redis/distributed.rb +205 -70
  23. data/lib/redis/errors.rb +48 -0
  24. data/lib/redis/hash_ring.rb +31 -73
  25. data/lib/redis/pipeline.rb +74 -18
  26. data/lib/redis/subscribe.rb +24 -13
  27. data/lib/redis/version.rb +3 -1
  28. metadata +63 -160
  29. data/.gitignore +0 -10
  30. data/.order +0 -169
  31. data/.travis.yml +0 -50
  32. data/.travis/Gemfile +0 -11
  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
@@ -1,33 +1,66 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "monitor"
2
- require "redis/errors"
4
+ require_relative "redis/errors"
3
5
 
4
6
  class Redis
7
+ class << self
8
+ attr_reader :exists_returns_integer
5
9
 
6
- def self.deprecate(message, trace = caller[0])
7
- $stderr.puts "\n#{message} (in #{trace})"
8
- end
10
+ def exists_returns_integer=(value)
11
+ unless value
12
+ message = "`Redis#exists(key)` will return an Integer by default in redis-rb 4.3. The option to explicitly " \
13
+ "disable this behaviour via `Redis.exists_returns_integer` will be removed in 5.0. You should use " \
14
+ "`exists?` instead."
15
+
16
+ ::Kernel.warn(message)
17
+ end
9
18
 
10
- attr :client
19
+ @exists_returns_integer = value
20
+ end
11
21
 
12
- # @deprecated The preferred way to create a new client object is using `#new`.
13
- # This method does not actually establish a connection to Redis,
14
- # in contrary to what you might expect.
15
- def self.connect(options = {})
16
- new(options)
22
+ attr_writer :current
17
23
  end
18
24
 
19
25
  def self.current
20
26
  @current ||= Redis.new
21
27
  end
22
28
 
23
- def self.current=(redis)
24
- @current = redis
25
- end
26
-
27
29
  include MonitorMixin
28
30
 
31
+ # Create a new client instance
32
+ #
33
+ # @param [Hash] options
34
+ # @option options [String] :url (value of the environment variable REDIS_URL) a Redis URL, for a TCP connection:
35
+ # `redis://:[password]@[hostname]:[port]/[db]` (password, port and database are optional), for a unix socket
36
+ # connection: `unix://[path to Redis socket]`. This overrides all other options.
37
+ # @option options [String] :host ("127.0.0.1") server hostname
38
+ # @option options [Integer] :port (6379) server port
39
+ # @option options [String] :path path to server socket (overrides host and port)
40
+ # @option options [Float] :timeout (5.0) timeout in seconds
41
+ # @option options [Float] :connect_timeout (same as timeout) timeout for initial connect in seconds
42
+ # @option options [String] :password Password to authenticate against server
43
+ # @option options [Integer] :db (0) Database to select after initial connect
44
+ # @option options [Symbol] :driver Driver to use, currently supported: `:ruby`, `:hiredis`, `:synchrony`
45
+ # @option options [String] :id ID for the client connection, assigns name to current connection by sending
46
+ # `CLIENT SETNAME`
47
+ # @option options [Hash, Integer] :tcp_keepalive Keepalive values, if Integer `intvl` and `probe` are calculated
48
+ # based on the value, if Hash `time`, `intvl` and `probes` can be specified as a Integer
49
+ # @option options [Integer] :reconnect_attempts Number of attempts trying to connect
50
+ # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
51
+ # @option options [Array] :sentinels List of sentinels to contact
52
+ # @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave`
53
+ # @option options [Array<String, Hash{Symbol => String, Integer}>] :cluster List of cluster nodes to contact
54
+ # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not
55
+ # @option options [Class] :connector Class of custom connector
56
+ #
57
+ # @return [Redis] a new client instance
29
58
  def initialize(options = {})
30
- @client = Client.new(options)
59
+ @options = options.dup
60
+ @cluster_mode = options.key?(:cluster)
61
+ client = @cluster_mode ? Cluster : Client
62
+ @original_client = @client = client.new(options)
63
+ @queue = Hash.new { |h, k| h[k] = [] }
31
64
 
32
65
  super() # Monitor#initialize
33
66
  end
@@ -37,7 +70,7 @@ class Redis
37
70
  end
38
71
 
39
72
  # Run code with the client reconnecting
40
- def with_reconnect(val=true, &blk)
73
+ def with_reconnect(val = true, &blk)
41
74
  synchronize do |client|
42
75
  client.with_reconnect(val, &blk)
43
76
  end
@@ -48,6 +81,66 @@ class Redis
48
81
  with_reconnect(false, &blk)
49
82
  end
50
83
 
84
+ # Test whether or not the client is connected
85
+ def connected?
86
+ @original_client.connected?
87
+ end
88
+
89
+ # Disconnect the client as quickly and silently as possible.
90
+ def close
91
+ @original_client.disconnect
92
+ end
93
+ alias disconnect! close
94
+
95
+ # Sends a command to Redis and returns its reply.
96
+ #
97
+ # Replies are converted to Ruby objects according to the RESP protocol, so
98
+ # you can expect a Ruby array, integer or nil when Redis sends one. Higher
99
+ # level transformations, such as converting an array of pairs into a Ruby
100
+ # hash, are up to consumers.
101
+ #
102
+ # Redis error replies are raised as Ruby exceptions.
103
+ def call(*command)
104
+ synchronize do |client|
105
+ client.call(command)
106
+ end
107
+ end
108
+
109
+ # Queues a command for pipelining.
110
+ #
111
+ # Commands in the queue are executed with the Redis#commit method.
112
+ #
113
+ # See http://redis.io/topics/pipelining for more details.
114
+ #
115
+ def queue(*command)
116
+ synchronize do
117
+ @queue[Thread.current.object_id] << command
118
+ end
119
+ end
120
+
121
+ # Sends all commands in the queue.
122
+ #
123
+ # See http://redis.io/topics/pipelining for more details.
124
+ #
125
+ def commit
126
+ synchronize do |client|
127
+ begin
128
+ pipeline = Pipeline.new(client)
129
+ @queue[Thread.current.object_id].each do |command|
130
+ pipeline.call(command)
131
+ end
132
+
133
+ client.call_pipelined(pipeline)
134
+ ensure
135
+ @queue.delete(Thread.current.object_id)
136
+ end
137
+ end
138
+ end
139
+
140
+ def _client
141
+ @client
142
+ end
143
+
51
144
  # Authenticate to the server.
52
145
  #
53
146
  # @param [String] password must match the password specified in the
@@ -55,27 +148,28 @@ class Redis
55
148
  # @return [String] `OK`
56
149
  def auth(password)
57
150
  synchronize do |client|
58
- client.call [:auth, password]
151
+ client.call([:auth, password])
59
152
  end
60
153
  end
61
154
 
62
155
  # Change the selected database for the current connection.
63
156
  #
64
- # @param [Fixnum] db zero-based index of the DB to use (0 to 15)
157
+ # @param [Integer] db zero-based index of the DB to use (0 to 15)
65
158
  # @return [String] `OK`
66
159
  def select(db)
67
160
  synchronize do |client|
68
161
  client.db = db
69
- client.call [:select, db]
162
+ client.call([:select, db])
70
163
  end
71
164
  end
72
165
 
73
166
  # Ping the server.
74
167
  #
168
+ # @param [optional, String] message
75
169
  # @return [String] `PONG`
76
- def ping
170
+ def ping(message = nil)
77
171
  synchronize do |client|
78
- client.call [:ping]
172
+ client.call([:ping, message].compact)
79
173
  end
80
174
  end
81
175
 
@@ -85,7 +179,7 @@ class Redis
85
179
  # @return [String]
86
180
  def echo(value)
87
181
  synchronize do |client|
88
- client.call [:echo, value]
182
+ client.call([:echo, value])
89
183
  end
90
184
  end
91
185
 
@@ -95,7 +189,7 @@ class Redis
95
189
  def quit
96
190
  synchronize do |client|
97
191
  begin
98
- client.call [:quit]
192
+ client.call([:quit])
99
193
  rescue ConnectionError
100
194
  ensure
101
195
  client.disconnect
@@ -108,7 +202,7 @@ class Redis
108
202
  # @return [String] `OK`
109
203
  def bgrewriteaof
110
204
  synchronize do |client|
111
- client.call [:bgrewriteaof]
205
+ client.call([:bgrewriteaof])
112
206
  end
113
207
  end
114
208
 
@@ -117,20 +211,39 @@ class Redis
117
211
  # @return [String] `OK`
118
212
  def bgsave
119
213
  synchronize do |client|
120
- client.call [:bgsave]
214
+ client.call([:bgsave])
121
215
  end
122
216
  end
123
217
 
124
218
  # Get or set server configuration parameters.
125
219
  #
126
- # @param [String] action e.g. `get`, `set`, `resetstat`
220
+ # @param [Symbol] action e.g. `:get`, `:set`, `:resetstat`
127
221
  # @return [String, Hash] string reply, or hash when retrieving more than one
128
222
  # property with `CONFIG GET`
129
223
  def config(action, *args)
130
224
  synchronize do |client|
131
- client.call [:config, action, *args] do |reply|
132
- if reply.kind_of?(Array) && action == :get
133
- Hash[*reply]
225
+ client.call([:config, action] + args) do |reply|
226
+ if reply.is_a?(Array) && action == :get
227
+ Hashify.call(reply)
228
+ else
229
+ reply
230
+ end
231
+ end
232
+ end
233
+ end
234
+
235
+ # Manage client connections.
236
+ #
237
+ # @param [String, Symbol] subcommand e.g. `kill`, `list`, `getname`, `setname`
238
+ # @return [String, Hash] depends on subcommand
239
+ def client(subcommand = nil, *args)
240
+ synchronize do |client|
241
+ client.call([:client, subcommand] + args) do |reply|
242
+ if subcommand.to_s == "list"
243
+ reply.lines.map do |line|
244
+ entries = line.chomp.split(/[ =]/)
245
+ Hash[entries.each_slice(2).to_a]
246
+ end
134
247
  else
135
248
  reply
136
249
  end
@@ -140,34 +253,46 @@ class Redis
140
253
 
141
254
  # Return the number of keys in the selected database.
142
255
  #
143
- # @return [Fixnum]
256
+ # @return [Integer]
144
257
  def dbsize
145
258
  synchronize do |client|
146
- client.call [:dbsize]
259
+ client.call([:dbsize])
147
260
  end
148
261
  end
149
262
 
150
263
  def debug(*args)
151
264
  synchronize do |client|
152
- client.call [:debug, *args]
265
+ client.call([:debug] + args)
153
266
  end
154
267
  end
155
268
 
156
269
  # Remove all keys from all databases.
157
270
  #
271
+ # @param [Hash] options
272
+ # - `:async => Boolean`: async flush (default: false)
158
273
  # @return [String] `OK`
159
- def flushall
274
+ def flushall(options = nil)
160
275
  synchronize do |client|
161
- client.call [:flushall]
276
+ if options && options[:async]
277
+ client.call(%i[flushall async])
278
+ else
279
+ client.call([:flushall])
280
+ end
162
281
  end
163
282
  end
164
283
 
165
284
  # Remove all keys from the current database.
166
285
  #
286
+ # @param [Hash] options
287
+ # - `:async => Boolean`: async flush (default: false)
167
288
  # @return [String] `OK`
168
- def flushdb
289
+ def flushdb(options = nil)
169
290
  synchronize do |client|
170
- client.call [:flushdb]
291
+ if options && options[:async]
292
+ client.call(%i[flushdb async])
293
+ else
294
+ client.call([:flushdb])
295
+ end
171
296
  end
172
297
  end
173
298
 
@@ -177,16 +302,15 @@ class Redis
177
302
  # @return [Hash<String, String>]
178
303
  def info(cmd = nil)
179
304
  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]
305
+ client.call([:info, cmd].compact) do |reply|
306
+ if reply.is_a?(String)
307
+ reply = HashifyInfo.call(reply)
185
308
 
186
309
  if cmd && cmd.to_s == "commandstats"
187
310
  # Extract nested hashes for INFO COMMANDSTATS
188
311
  reply = Hash[reply.map do |k, v|
189
- [k[/^cmdstat_(.*)$/, 1], Hash[*v.split(/,|=/)]]
312
+ v = v.split(",").map { |e| e.split("=") }
313
+ [k[/^cmdstat_(.*)$/, 1], Hash[v]]
190
314
  end]
191
315
  end
192
316
  end
@@ -198,10 +322,10 @@ class Redis
198
322
 
199
323
  # Get the UNIX time stamp of the last successful save to disk.
200
324
  #
201
- # @return [Fixnum]
325
+ # @return [Integer]
202
326
  def lastsave
203
327
  synchronize do |client|
204
- client.call [:lastsave]
328
+ client.call([:lastsave])
205
329
  end
206
330
  end
207
331
 
@@ -222,7 +346,7 @@ class Redis
222
346
  # @return [String]
223
347
  def save
224
348
  synchronize do |client|
225
- client.call [:save]
349
+ client.call([:save])
226
350
  end
227
351
  end
228
352
 
@@ -231,7 +355,7 @@ class Redis
231
355
  synchronize do |client|
232
356
  client.with_reconnect(false) do
233
357
  begin
234
- client.call [:shutdown]
358
+ client.call([:shutdown])
235
359
  rescue ConnectionError
236
360
  # This means Redis has probably exited.
237
361
  nil
@@ -243,16 +367,16 @@ class Redis
243
367
  # Make the server a slave of another instance, or promote it as master.
244
368
  def slaveof(host, port)
245
369
  synchronize do |client|
246
- client.call [:slaveof, host, port]
370
+ client.call([:slaveof, host, port])
247
371
  end
248
372
  end
249
373
 
250
374
  # Interact with the slowlog (get, len, reset)
251
375
  #
252
376
  # @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)
377
+ # @param [Integer] length maximum number of entries to return
378
+ # @return [Array<String>, Integer, String] depends on subcommand
379
+ def slowlog(subcommand, length = nil)
256
380
  synchronize do |client|
257
381
  args = [:slowlog, subcommand]
258
382
  args << length if length
@@ -263,7 +387,7 @@ class Redis
263
387
  # Internal command used for replication.
264
388
  def sync
265
389
  synchronize do |client|
266
- client.call [:sync]
390
+ client.call([:sync])
267
391
  end
268
392
  end
269
393
 
@@ -272,12 +396,12 @@ class Redis
272
396
  # @example
273
397
  # r.time # => [ 1333093196, 606806 ]
274
398
  #
275
- # @return [Array<Fixnum>] tuple of seconds since UNIX epoch and
399
+ # @return [Array<Integer>] tuple of seconds since UNIX epoch and
276
400
  # microseconds in the current second
277
401
  def time
278
402
  synchronize do |client|
279
- client.call [:time] do |reply|
280
- reply.map(&:to_i) if reply
403
+ client.call([:time]) do |reply|
404
+ reply&.map(&:to_i)
281
405
  end
282
406
  end
283
407
  end
@@ -288,93 +412,199 @@ class Redis
288
412
  # @return [Boolean] whether the timeout was removed or not
289
413
  def persist(key)
290
414
  synchronize do |client|
291
- client.call [:persist, key], &_boolify
415
+ client.call([:persist, key], &Boolify)
292
416
  end
293
417
  end
294
418
 
295
419
  # Set a key's time to live in seconds.
296
420
  #
297
421
  # @param [String] key
298
- # @param [Fixnum] seconds time to live
422
+ # @param [Integer] seconds time to live
299
423
  # @return [Boolean] whether the timeout was set or not
300
424
  def expire(key, seconds)
301
425
  synchronize do |client|
302
- client.call [:expire, key, seconds], &_boolify
426
+ client.call([:expire, key, seconds], &Boolify)
303
427
  end
304
428
  end
305
429
 
306
430
  # Set the expiration for a key as a UNIX timestamp.
307
431
  #
308
432
  # @param [String] key
309
- # @param [Fixnum] unix_time expiry time specified as a UNIX timestamp
433
+ # @param [Integer] unix_time expiry time specified as a UNIX timestamp
310
434
  # @return [Boolean] whether the timeout was set or not
311
435
  def expireat(key, unix_time)
312
436
  synchronize do |client|
313
- client.call [:expireat, key, unix_time], &_boolify
437
+ client.call([:expireat, key, unix_time], &Boolify)
314
438
  end
315
439
  end
316
440
 
317
441
  # Get the time to live (in seconds) for a key.
318
442
  #
319
443
  # @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
444
+ # @return [Integer] remaining time to live in seconds.
445
+ #
446
+ # In Redis 2.6 or older the command returns -1 if the key does not exist or if
447
+ # the key exist but has no associated expire.
448
+ #
449
+ # Starting with Redis 2.8 the return value in case of error changed:
450
+ #
451
+ # - The command returns -2 if the key does not exist.
452
+ # - The command returns -1 if the key exists but has no associated expire.
322
453
  def ttl(key)
323
454
  synchronize do |client|
324
- client.call [:ttl, key]
455
+ client.call([:ttl, key])
325
456
  end
326
457
  end
327
458
 
328
459
  # Set a key's time to live in milliseconds.
329
460
  #
330
461
  # @param [String] key
331
- # @param [Fixnum] milliseconds time to live
462
+ # @param [Integer] milliseconds time to live
332
463
  # @return [Boolean] whether the timeout was set or not
333
464
  def pexpire(key, milliseconds)
334
465
  synchronize do |client|
335
- client.call [:pexpire, key, milliseconds], &_boolify
466
+ client.call([:pexpire, key, milliseconds], &Boolify)
336
467
  end
337
468
  end
338
469
 
339
470
  # Set the expiration for a key as number of milliseconds from UNIX Epoch.
340
471
  #
341
472
  # @param [String] key
342
- # @param [Fixnum] ms_unix_time expiry time specified as number of milliseconds from UNIX Epoch.
473
+ # @param [Integer] ms_unix_time expiry time specified as number of milliseconds from UNIX Epoch.
343
474
  # @return [Boolean] whether the timeout was set or not
344
475
  def pexpireat(key, ms_unix_time)
345
476
  synchronize do |client|
346
- client.call [:pexpireat, key, ms_unix_time], &_boolify
477
+ client.call([:pexpireat, key, ms_unix_time], &Boolify)
347
478
  end
348
479
  end
349
480
 
350
481
  # Get the time to live (in milliseconds) for a key.
351
482
  #
352
483
  # @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
484
+ # @return [Integer] remaining time to live in milliseconds
485
+ # In Redis 2.6 or older the command returns -1 if the key does not exist or if
486
+ # the key exist but has no associated expire.
487
+ #
488
+ # Starting with Redis 2.8 the return value in case of error changed:
489
+ #
490
+ # - The command returns -2 if the key does not exist.
491
+ # - The command returns -1 if the key exists but has no associated expire.
355
492
  def pttl(key)
356
493
  synchronize do |client|
357
- client.call [:pttl, key]
494
+ client.call([:pttl, key])
495
+ end
496
+ end
497
+
498
+ # Return a serialized version of the value stored at a key.
499
+ #
500
+ # @param [String] key
501
+ # @return [String] serialized_value
502
+ def dump(key)
503
+ synchronize do |client|
504
+ client.call([:dump, key])
505
+ end
506
+ end
507
+
508
+ # Create a key using the serialized value, previously obtained using DUMP.
509
+ #
510
+ # @param [String] key
511
+ # @param [String] ttl
512
+ # @param [String] serialized_value
513
+ # @param [Hash] options
514
+ # - `:replace => Boolean`: if false, raises an error if key already exists
515
+ # @raise [Redis::CommandError]
516
+ # @return [String] `"OK"`
517
+ def restore(key, ttl, serialized_value, replace: nil)
518
+ args = [:restore, key, ttl, serialized_value]
519
+ args << 'REPLACE' if replace
520
+
521
+ synchronize do |client|
522
+ client.call(args)
358
523
  end
359
524
  end
360
525
 
526
+ # Transfer a key from the connected instance to another instance.
527
+ #
528
+ # @param [String, Array<String>] key
529
+ # @param [Hash] options
530
+ # - `:host => String`: host of instance to migrate to
531
+ # - `:port => Integer`: port of instance to migrate to
532
+ # - `:db => Integer`: database to migrate to (default: same as source)
533
+ # - `:timeout => Integer`: timeout (default: same as connection timeout)
534
+ # - `:copy => Boolean`: Do not remove the key from the local instance.
535
+ # - `:replace => Boolean`: Replace existing key on the remote instance.
536
+ # @return [String] `"OK"`
537
+ def migrate(key, options)
538
+ args = [:migrate]
539
+ args << (options[:host] || raise(':host not specified'))
540
+ args << (options[:port] || raise(':port not specified'))
541
+ args << (key.is_a?(String) ? key : '')
542
+ args << (options[:db] || @client.db).to_i
543
+ args << (options[:timeout] || @client.timeout).to_i
544
+ args << 'COPY' if options[:copy]
545
+ args << 'REPLACE' if options[:replace]
546
+ args += ['KEYS', *key] if key.is_a?(Array)
547
+
548
+ synchronize { |client| client.call(args) }
549
+ end
550
+
361
551
  # Delete one or more keys.
362
552
  #
363
553
  # @param [String, Array<String>] keys
364
- # @return [Fixnum] number of keys that were deleted
554
+ # @return [Integer] number of keys that were deleted
365
555
  def del(*keys)
366
556
  synchronize do |client|
367
- client.call [:del, *keys]
557
+ client.call([:del] + keys)
368
558
  end
369
559
  end
370
560
 
371
- # Determine if a key exists.
561
+ # Unlink one or more keys.
372
562
  #
373
- # @param [String] key
563
+ # @param [String, Array<String>] keys
564
+ # @return [Integer] number of keys that were unlinked
565
+ def unlink(*keys)
566
+ synchronize do |client|
567
+ client.call([:unlink] + keys)
568
+ end
569
+ end
570
+
571
+ # Determine how many of the keys exists.
572
+ #
573
+ # @param [String, Array<String>] keys
574
+ # @return [Integer]
575
+ def exists(*keys)
576
+ if !Redis.exists_returns_integer && keys.size == 1
577
+ if Redis.exists_returns_integer.nil?
578
+ message = "`Redis#exists(key)` will return an Integer in redis-rb 4.3. `exists?` returns a boolean, you " \
579
+ "should use it instead. To opt-in to the new behavior now you can set Redis.exists_returns_integer = " \
580
+ "true. To disable this message and keep the current (boolean) behaviour of 'exists' you can set " \
581
+ "`Redis.exists_returns_integer = false`, but this option will be removed in 5.0. " \
582
+ "(#{::Kernel.caller(1, 1).first})\n"
583
+
584
+ ::Kernel.warn(message)
585
+ end
586
+
587
+ exists?(*keys)
588
+ else
589
+ _exists(*keys)
590
+ end
591
+ end
592
+
593
+ def _exists(*keys)
594
+ synchronize do |client|
595
+ client.call([:exists, *keys])
596
+ end
597
+ end
598
+
599
+ # Determine if any of the keys exists.
600
+ #
601
+ # @param [String, Array<String>] keys
374
602
  # @return [Boolean]
375
- def exists(key)
603
+ def exists?(*keys)
376
604
  synchronize do |client|
377
- client.call [:exists, key], &_boolify
605
+ client.call([:exists, *keys]) do |value|
606
+ value > 0
607
+ end
378
608
  end
379
609
  end
380
610
 
@@ -384,8 +614,8 @@ class Redis
384
614
  # @return [Array<String>]
385
615
  def keys(pattern = "*")
386
616
  synchronize do |client|
387
- client.call [:keys, pattern] do |reply|
388
- if reply.kind_of?(String)
617
+ client.call([:keys, pattern]) do |reply|
618
+ if reply.is_a?(String)
389
619
  reply.split(" ")
390
620
  else
391
621
  reply
@@ -407,21 +637,21 @@ class Redis
407
637
  # # => "OK"
408
638
  # redis.exists "foo"
409
639
  # # => true
410
- # resis.get "foo"
640
+ # redis.get "foo"
411
641
  # # => "bar"
412
642
  #
413
643
  # @param [String] key
414
- # @param [Fixnum] db
644
+ # @param [Integer] db
415
645
  # @return [Boolean] whether the key was moved or not
416
646
  def move(key, db)
417
647
  synchronize do |client|
418
- client.call [:move, key, db], &_boolify
648
+ client.call([:move, key, db], &Boolify)
419
649
  end
420
650
  end
421
651
 
422
652
  def object(*args)
423
653
  synchronize do |client|
424
- client.call [:object, *args]
654
+ client.call([:object] + args)
425
655
  end
426
656
  end
427
657
 
@@ -430,7 +660,7 @@ class Redis
430
660
  # @return [String]
431
661
  def randomkey
432
662
  synchronize do |client|
433
- client.call [:randomkey]
663
+ client.call([:randomkey])
434
664
  end
435
665
  end
436
666
 
@@ -441,7 +671,7 @@ class Redis
441
671
  # @return [String] `OK`
442
672
  def rename(old_name, new_name)
443
673
  synchronize do |client|
444
- client.call [:rename, old_name, new_name]
674
+ client.call([:rename, old_name, new_name])
445
675
  end
446
676
  end
447
677
 
@@ -452,7 +682,7 @@ class Redis
452
682
  # @return [Boolean] whether the key was renamed or not
453
683
  def renamenx(old_name, new_name)
454
684
  synchronize do |client|
455
- client.call [:renamenx, old_name, new_name], &_boolify
685
+ client.call([:renamenx, old_name, new_name], &Boolify)
456
686
  end
457
687
  end
458
688
 
@@ -475,36 +705,33 @@ class Redis
475
705
  # - `:order => String`: combination of `ASC`, `DESC` and optionally `ALPHA`
476
706
  # - `:store => String`: key to store the result at
477
707
  #
478
- # @return [Array<String>, Array<Array<String>>, Fixnum]
708
+ # @return [Array<String>, Array<Array<String>>, Integer]
479
709
  # - when `:get` is not specified, or holds a single element, an array of elements
480
710
  # - when `:get` is specified, and holds more than one element, an array of
481
711
  # elements where every element is an array with the result for every
482
712
  # element specified in `:get`
483
713
  # - when `:store` is specified, the number of elements in the stored result
484
- def sort(key, options = {})
485
- args = []
486
-
487
- by = options[:by]
488
- args.concat ["BY", by] if by
714
+ def sort(key, by: nil, limit: nil, get: nil, order: nil, store: nil)
715
+ args = [:sort, key]
716
+ args << "BY" << by if by
489
717
 
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?
718
+ if limit
719
+ args << "LIMIT"
720
+ args.concat(limit)
721
+ end
495
722
 
496
- order = options[:order]
497
- args.concat order.split(" ") if order
723
+ get = Array(get)
724
+ get.each do |item|
725
+ args << "GET" << item
726
+ end
498
727
 
499
- store = options[:store]
500
- args.concat ["STORE", store] if store
728
+ args.concat(order.split(" ")) if order
729
+ args << "STORE" << store if store
501
730
 
502
731
  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
732
+ client.call(args) do |reply|
733
+ if get.size > 1 && !store
734
+ reply.each_slice(get.size).to_a if reply
508
735
  else
509
736
  reply
510
737
  end
@@ -518,7 +745,7 @@ class Redis
518
745
  # @return [String] `string`, `list`, `set`, `zset`, `hash` or `none`
519
746
  def type(key)
520
747
  synchronize do |client|
521
- client.call [:type, key]
748
+ client.call([:type, key])
522
749
  end
523
750
  end
524
751
 
@@ -529,10 +756,10 @@ class Redis
529
756
  # # => 4
530
757
  #
531
758
  # @param [String] key
532
- # @return [Fixnum] value after decrementing it
759
+ # @return [Integer] value after decrementing it
533
760
  def decr(key)
534
761
  synchronize do |client|
535
- client.call [:decr, key]
762
+ client.call([:decr, key])
536
763
  end
537
764
  end
538
765
 
@@ -543,11 +770,11 @@ class Redis
543
770
  # # => 0
544
771
  #
545
772
  # @param [String] key
546
- # @param [Fixnum] decrement
547
- # @return [Fixnum] value after decrementing it
773
+ # @param [Integer] decrement
774
+ # @return [Integer] value after decrementing it
548
775
  def decrby(key, decrement)
549
776
  synchronize do |client|
550
- client.call [:decrby, key, decrement]
777
+ client.call([:decrby, key, decrement])
551
778
  end
552
779
  end
553
780
 
@@ -558,10 +785,10 @@ class Redis
558
785
  # # => 6
559
786
  #
560
787
  # @param [String] key
561
- # @return [Fixnum] value after incrementing it
788
+ # @return [Integer] value after incrementing it
562
789
  def incr(key)
563
790
  synchronize do |client|
564
- client.call [:incr, key]
791
+ client.call([:incr, key])
565
792
  end
566
793
  end
567
794
 
@@ -572,11 +799,11 @@ class Redis
572
799
  # # => 10
573
800
  #
574
801
  # @param [String] key
575
- # @param [Fixnum] increment
576
- # @return [Fixnum] value after incrementing it
802
+ # @param [Integer] increment
803
+ # @return [Integer] value after incrementing it
577
804
  def incrby(key, increment)
578
805
  synchronize do |client|
579
- client.call [:incrby, key, increment]
806
+ client.call([:incrby, key, increment])
580
807
  end
581
808
  end
582
809
 
@@ -591,9 +818,7 @@ class Redis
591
818
  # @return [Float] value after incrementing it
592
819
  def incrbyfloat(key, increment)
593
820
  synchronize do |client|
594
- client.call [:incrbyfloat, key, increment] do |reply|
595
- Float(reply) if reply
596
- end
821
+ client.call([:incrbyfloat, key, increment], &Floatify)
597
822
  end
598
823
  end
599
824
 
@@ -601,36 +826,51 @@ class Redis
601
826
  #
602
827
  # @param [String] key
603
828
  # @param [String] value
604
- # @return `"OK"`
605
- def set(key, value)
606
- synchronize do |client|
607
- client.call [:set, key, value]
829
+ # @param [Hash] options
830
+ # - `:ex => Integer`: Set the specified expire time, in seconds.
831
+ # - `:px => Integer`: Set the specified expire time, in milliseconds.
832
+ # - `:nx => true`: Only set the key if it does not already exist.
833
+ # - `:xx => true`: Only set the key if it already exist.
834
+ # - `:keepttl => true`: Retain the time to live associated with the key.
835
+ # @return [String, Boolean] `"OK"` or true, false if `:nx => true` or `:xx => true`
836
+ def set(key, value, ex: nil, px: nil, nx: nil, xx: nil, keepttl: nil)
837
+ args = [:set, key, value.to_s]
838
+ args << "EX" << ex if ex
839
+ args << "PX" << px if px
840
+ args << "NX" if nx
841
+ args << "XX" if xx
842
+ args << "KEEPTTL" if keepttl
843
+
844
+ synchronize do |client|
845
+ if nx || xx
846
+ client.call(args, &BoolifySet)
847
+ else
848
+ client.call(args)
849
+ end
608
850
  end
609
851
  end
610
852
 
611
- alias :[]= :set
612
-
613
853
  # Set the time to live in seconds of a key.
614
854
  #
615
855
  # @param [String] key
616
- # @param [Fixnum] ttl
856
+ # @param [Integer] ttl
617
857
  # @param [String] value
618
- # @return `"OK"`
858
+ # @return [String] `"OK"`
619
859
  def setex(key, ttl, value)
620
860
  synchronize do |client|
621
- client.call [:setex, key, ttl, value]
861
+ client.call([:setex, key, ttl, value.to_s])
622
862
  end
623
863
  end
624
864
 
625
865
  # Set the time to live in milliseconds of a key.
626
866
  #
627
867
  # @param [String] key
628
- # @param [Fixnum] ttl
868
+ # @param [Integer] ttl
629
869
  # @param [String] value
630
- # @return `"OK"`
870
+ # @return [String] `"OK"`
631
871
  def psetex(key, ttl, value)
632
872
  synchronize do |client|
633
- client.call [:psetex, key, ttl, value]
873
+ client.call([:psetex, key, ttl, value.to_s])
634
874
  end
635
875
  end
636
876
 
@@ -641,7 +881,7 @@ class Redis
641
881
  # @return [Boolean] whether the key was set or not
642
882
  def setnx(key, value)
643
883
  synchronize do |client|
644
- client.call [:setnx, key, value], &_boolify
884
+ client.call([:setnx, key, value.to_s], &Boolify)
645
885
  end
646
886
  end
647
887
 
@@ -652,12 +892,12 @@ class Redis
652
892
  # # => "OK"
653
893
  #
654
894
  # @param [Array<String>] args array of keys and values
655
- # @return `"OK"`
895
+ # @return [String] `"OK"`
656
896
  #
657
897
  # @see #mapped_mset
658
898
  def mset(*args)
659
899
  synchronize do |client|
660
- client.call [:mset, *args]
900
+ client.call([:mset] + args)
661
901
  end
662
902
  end
663
903
 
@@ -668,11 +908,11 @@ class Redis
668
908
  # # => "OK"
669
909
  #
670
910
  # @param [Hash] hash keys mapping to values
671
- # @return `"OK"`
911
+ # @return [String] `"OK"`
672
912
  #
673
913
  # @see #mset
674
914
  def mapped_mset(hash)
675
- mset(*hash.to_a.flatten)
915
+ mset(hash.to_a.flatten)
676
916
  end
677
917
 
678
918
  # Set one or more values, only if none of the keys exist.
@@ -687,14 +927,14 @@ class Redis
687
927
  # @see #mapped_msetnx
688
928
  def msetnx(*args)
689
929
  synchronize do |client|
690
- client.call [:msetnx, *args], &_boolify
930
+ client.call([:msetnx, *args], &Boolify)
691
931
  end
692
932
  end
693
933
 
694
934
  # Set one or more values, only if none of the keys exist.
695
935
  #
696
936
  # @example
697
- # redis.msetnx({ "key1" => "v1", "key2" => "v2" })
937
+ # redis.mapped_msetnx({ "key1" => "v1", "key2" => "v2" })
698
938
  # # => true
699
939
  #
700
940
  # @param [Hash] hash keys mapping to values
@@ -702,7 +942,7 @@ class Redis
702
942
  #
703
943
  # @see #msetnx
704
944
  def mapped_msetnx(hash)
705
- msetnx(*hash.to_a.flatten)
945
+ msetnx(hash.to_a.flatten)
706
946
  end
707
947
 
708
948
  # Get the value of a key.
@@ -711,16 +951,14 @@ class Redis
711
951
  # @return [String]
712
952
  def get(key)
713
953
  synchronize do |client|
714
- client.call [:get, key]
954
+ client.call([:get, key])
715
955
  end
716
956
  end
717
957
 
718
- alias :[] :get
719
-
720
958
  # Get the values of all the given keys.
721
959
  #
722
960
  # @example
723
- # redis.mget("key1", "key1")
961
+ # redis.mget("key1", "key2")
724
962
  # # => ["v1", "v2"]
725
963
  #
726
964
  # @param [Array<String>] keys
@@ -729,14 +967,14 @@ class Redis
729
967
  # @see #mapped_mget
730
968
  def mget(*keys, &blk)
731
969
  synchronize do |client|
732
- client.call [:mget, *keys], &blk
970
+ client.call([:mget, *keys], &blk)
733
971
  end
734
972
  end
735
973
 
736
974
  # Get the values of all the given keys.
737
975
  #
738
976
  # @example
739
- # redis.mapped_mget("key1", "key1")
977
+ # redis.mapped_mget("key1", "key2")
740
978
  # # => { "key1" => "v1", "key2" => "v2" }
741
979
  #
742
980
  # @param [Array<String>] keys array of keys
@@ -745,12 +983,8 @@ class Redis
745
983
  # @see #mget
746
984
  def mapped_mget(*keys)
747
985
  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
986
+ if reply.is_a?(Array)
987
+ Hash[keys.zip(reply)]
754
988
  else
755
989
  reply
756
990
  end
@@ -760,48 +994,48 @@ class Redis
760
994
  # Overwrite part of a string at key starting at the specified offset.
761
995
  #
762
996
  # @param [String] key
763
- # @param [Fixnum] offset byte offset
997
+ # @param [Integer] offset byte offset
764
998
  # @param [String] value
765
- # @return [Fixnum] length of the string after it was modified
999
+ # @return [Integer] length of the string after it was modified
766
1000
  def setrange(key, offset, value)
767
1001
  synchronize do |client|
768
- client.call [:setrange, key, offset, value]
1002
+ client.call([:setrange, key, offset, value.to_s])
769
1003
  end
770
1004
  end
771
1005
 
772
1006
  # Get a substring of the string stored at a key.
773
1007
  #
774
1008
  # @param [String] key
775
- # @param [Fixnum] start zero-based start offset
776
- # @param [Fixnum] stop zero-based end offset. Use -1 for representing
1009
+ # @param [Integer] start zero-based start offset
1010
+ # @param [Integer] stop zero-based end offset. Use -1 for representing
777
1011
  # the end of the string
778
- # @return [Fixnum] `0` or `1`
1012
+ # @return [Integer] `0` or `1`
779
1013
  def getrange(key, start, stop)
780
1014
  synchronize do |client|
781
- client.call [:getrange, key, start, stop]
1015
+ client.call([:getrange, key, start, stop])
782
1016
  end
783
1017
  end
784
1018
 
785
1019
  # Sets or clears the bit at offset in the string value stored at key.
786
1020
  #
787
1021
  # @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`
1022
+ # @param [Integer] offset bit offset
1023
+ # @param [Integer] value bit value `0` or `1`
1024
+ # @return [Integer] the original bit value stored at `offset`
791
1025
  def setbit(key, offset, value)
792
1026
  synchronize do |client|
793
- client.call [:setbit, key, offset, value]
1027
+ client.call([:setbit, key, offset, value])
794
1028
  end
795
1029
  end
796
1030
 
797
1031
  # Returns the bit value at offset in the string value stored at key.
798
1032
  #
799
1033
  # @param [String] key
800
- # @param [Fixnum] offset bit offset
801
- # @return [Fixnum] `0` or `1`
1034
+ # @param [Integer] offset bit offset
1035
+ # @return [Integer] `0` or `1`
802
1036
  def getbit(key, offset)
803
1037
  synchronize do |client|
804
- client.call [:getbit, key, offset]
1038
+ client.call([:getbit, key, offset])
805
1039
  end
806
1040
  end
807
1041
 
@@ -809,10 +1043,53 @@ class Redis
809
1043
  #
810
1044
  # @param [String] key
811
1045
  # @param [String] value value to append
812
- # @return [Fixnum] length of the string after appending
1046
+ # @return [Integer] length of the string after appending
813
1047
  def append(key, value)
814
1048
  synchronize do |client|
815
- client.call [:append, key, value]
1049
+ client.call([:append, key, value])
1050
+ end
1051
+ end
1052
+
1053
+ # Count the number of set bits in a range of the string value stored at key.
1054
+ #
1055
+ # @param [String] key
1056
+ # @param [Integer] start start index
1057
+ # @param [Integer] stop stop index
1058
+ # @return [Integer] the number of bits set to 1
1059
+ def bitcount(key, start = 0, stop = -1)
1060
+ synchronize do |client|
1061
+ client.call([:bitcount, key, start, stop])
1062
+ end
1063
+ end
1064
+
1065
+ # Perform a bitwise operation between strings and store the resulting string in a key.
1066
+ #
1067
+ # @param [String] operation e.g. `and`, `or`, `xor`, `not`
1068
+ # @param [String] destkey destination key
1069
+ # @param [String, Array<String>] keys one or more source keys to perform `operation`
1070
+ # @return [Integer] the length of the string stored in `destkey`
1071
+ def bitop(operation, destkey, *keys)
1072
+ synchronize do |client|
1073
+ client.call([:bitop, operation, destkey, *keys])
1074
+ end
1075
+ end
1076
+
1077
+ # Return the position of the first bit set to 1 or 0 in a string.
1078
+ #
1079
+ # @param [String] key
1080
+ # @param [Integer] bit whether to look for the first 1 or 0 bit
1081
+ # @param [Integer] start start index
1082
+ # @param [Integer] stop stop index
1083
+ # @return [Integer] the position of the first 1/0 bit.
1084
+ # -1 if looking for 1 and it is not found or start and stop are given.
1085
+ def bitpos(key, bit, start = nil, stop = nil)
1086
+ raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
1087
+
1088
+ synchronize do |client|
1089
+ command = [:bitpos, key, bit]
1090
+ command << start if start
1091
+ command << stop if stop
1092
+ client.call(command)
816
1093
  end
817
1094
  end
818
1095
 
@@ -824,39 +1101,39 @@ class Redis
824
1101
  # did not exist
825
1102
  def getset(key, value)
826
1103
  synchronize do |client|
827
- client.call [:getset, key, value]
1104
+ client.call([:getset, key, value.to_s])
828
1105
  end
829
1106
  end
830
1107
 
831
1108
  # Get the length of the value stored in a key.
832
1109
  #
833
1110
  # @param [String] key
834
- # @return [Fixnum] the length of the value stored in the key, or 0
1111
+ # @return [Integer] the length of the value stored in the key, or 0
835
1112
  # if the key does not exist
836
1113
  def strlen(key)
837
1114
  synchronize do |client|
838
- client.call [:strlen, key]
1115
+ client.call([:strlen, key])
839
1116
  end
840
1117
  end
841
1118
 
842
1119
  # Get the length of a list.
843
1120
  #
844
1121
  # @param [String] key
845
- # @return [Fixnum]
1122
+ # @return [Integer]
846
1123
  def llen(key)
847
1124
  synchronize do |client|
848
- client.call [:llen, key]
1125
+ client.call([:llen, key])
849
1126
  end
850
1127
  end
851
1128
 
852
1129
  # Prepend one or more values to a list, creating the list if it doesn't exist
853
1130
  #
854
1131
  # @param [String] key
855
- # @param [String] value
856
- # @return [Fixnum] the length of the list after the push operation
1132
+ # @param [String, Array<String>] value string value, or array of string values to push
1133
+ # @return [Integer] the length of the list after the push operation
857
1134
  def lpush(key, value)
858
1135
  synchronize do |client|
859
- client.call [:lpush, key, value]
1136
+ client.call([:lpush, key, value])
860
1137
  end
861
1138
  end
862
1139
 
@@ -864,21 +1141,21 @@ class Redis
864
1141
  #
865
1142
  # @param [String] key
866
1143
  # @param [String] value
867
- # @return [Fixnum] the length of the list after the push operation
1144
+ # @return [Integer] the length of the list after the push operation
868
1145
  def lpushx(key, value)
869
1146
  synchronize do |client|
870
- client.call [:lpushx, key, value]
1147
+ client.call([:lpushx, key, value])
871
1148
  end
872
1149
  end
873
1150
 
874
1151
  # Append one or more values to a list, creating the list if it doesn't exist
875
1152
  #
876
1153
  # @param [String] key
877
- # @param [String] value
878
- # @return [Fixnum] the length of the list after the push operation
1154
+ # @param [String, Array<String>] value string value, or array of string values to push
1155
+ # @return [Integer] the length of the list after the push operation
879
1156
  def rpush(key, value)
880
1157
  synchronize do |client|
881
- client.call [:rpush, key, value]
1158
+ client.call([:rpush, key, value])
882
1159
  end
883
1160
  end
884
1161
 
@@ -886,10 +1163,10 @@ class Redis
886
1163
  #
887
1164
  # @param [String] key
888
1165
  # @param [String] value
889
- # @return [Fixnum] the length of the list after the push operation
1166
+ # @return [Integer] the length of the list after the push operation
890
1167
  def rpushx(key, value)
891
1168
  synchronize do |client|
892
- client.call [:rpushx, key, value]
1169
+ client.call([:rpushx, key, value])
893
1170
  end
894
1171
  end
895
1172
 
@@ -899,7 +1176,7 @@ class Redis
899
1176
  # @return [String]
900
1177
  def lpop(key)
901
1178
  synchronize do |client|
902
- client.call [:lpop, key]
1179
+ client.call([:lpop, key])
903
1180
  end
904
1181
  end
905
1182
 
@@ -909,7 +1186,7 @@ class Redis
909
1186
  # @return [String]
910
1187
  def rpop(key)
911
1188
  synchronize do |client|
912
- client.call [:rpop, key]
1189
+ client.call([:rpop, key])
913
1190
  end
914
1191
  end
915
1192
 
@@ -920,30 +1197,31 @@ class Redis
920
1197
  # @return [nil, String] the element, or nil when the source key does not exist
921
1198
  def rpoplpush(source, destination)
922
1199
  synchronize do |client|
923
- client.call [:rpoplpush, source, destination]
1200
+ client.call([:rpoplpush, source, destination])
924
1201
  end
925
1202
  end
926
1203
 
927
- def _bpop(cmd, args)
928
- options = {}
929
-
930
- case args.last
931
- when Hash
1204
+ def _bpop(cmd, args, &blk)
1205
+ timeout = if args.last.is_a?(Hash)
932
1206
  options = args.pop
933
- when Integer
1207
+ options[:timeout]
1208
+ elsif args.last.respond_to?(:to_int)
934
1209
  # Issue deprecation notice in obnoxious mode...
935
- options[:timeout] = args.pop
1210
+ args.pop.to_int
936
1211
  end
937
1212
 
1213
+ timeout ||= 0
1214
+
938
1215
  if args.size > 1
939
1216
  # Issue deprecation notice in obnoxious mode...
940
1217
  end
941
1218
 
942
1219
  keys = args.flatten
943
- timeout = options[:timeout] || 0
944
1220
 
945
1221
  synchronize do |client|
946
- client.call_without_timeout [cmd, keys, timeout]
1222
+ command = [cmd, keys, timeout]
1223
+ timeout += client.timeout if timeout > 0
1224
+ client.call_with_timeout(command, timeout, &blk)
947
1225
  end
948
1226
  end
949
1227
 
@@ -963,7 +1241,7 @@ class Redis
963
1241
  # @param [String, Array<String>] keys one or more keys to perform the
964
1242
  # blocking pop on
965
1243
  # @param [Hash] options
966
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1244
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
967
1245
  #
968
1246
  # @return [nil, [String, String]]
969
1247
  # - `nil` when the operation timed out
@@ -977,7 +1255,7 @@ class Redis
977
1255
  # @param [String, Array<String>] keys one or more keys to perform the
978
1256
  # blocking pop on
979
1257
  # @param [Hash] options
980
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1258
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
981
1259
  #
982
1260
  # @return [nil, [String, String]]
983
1261
  # - `nil` when the operation timed out
@@ -994,33 +1272,27 @@ class Redis
994
1272
  # @param [String] source source key
995
1273
  # @param [String] destination destination key
996
1274
  # @param [Hash] options
997
- # - `:timeout => Fixnum`: timeout in seconds, defaults to no timeout
1275
+ # - `:timeout => Integer`: timeout in seconds, defaults to no timeout
998
1276
  #
999
1277
  # @return [nil, String]
1000
1278
  # - `nil` when the operation timed out
1001
1279
  # - 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
-
1280
+ def brpoplpush(source, destination, deprecated_timeout = 0, timeout: deprecated_timeout)
1011
1281
  synchronize do |client|
1012
- client.call_without_timeout [:brpoplpush, source, destination, timeout]
1282
+ command = [:brpoplpush, source, destination, timeout]
1283
+ timeout += client.timeout if timeout > 0
1284
+ client.call_with_timeout(command, timeout)
1013
1285
  end
1014
1286
  end
1015
1287
 
1016
1288
  # Get an element from a list by its index.
1017
1289
  #
1018
1290
  # @param [String] key
1019
- # @param [Fixnum] index
1291
+ # @param [Integer] index
1020
1292
  # @return [String]
1021
1293
  def lindex(key, index)
1022
1294
  synchronize do |client|
1023
- client.call [:lindex, key, index]
1295
+ client.call([:lindex, key, index])
1024
1296
  end
1025
1297
  end
1026
1298
 
@@ -1030,72 +1302,72 @@ class Redis
1030
1302
  # @param [String, Symbol] where `BEFORE` or `AFTER`
1031
1303
  # @param [String] pivot reference element
1032
1304
  # @param [String] value
1033
- # @return [Fixnum] length of the list after the insert operation, or `-1`
1305
+ # @return [Integer] length of the list after the insert operation, or `-1`
1034
1306
  # when the element `pivot` was not found
1035
1307
  def linsert(key, where, pivot, value)
1036
1308
  synchronize do |client|
1037
- client.call [:linsert, key, where, pivot, value]
1309
+ client.call([:linsert, key, where, pivot, value])
1038
1310
  end
1039
1311
  end
1040
1312
 
1041
1313
  # Get a range of elements from a list.
1042
1314
  #
1043
1315
  # @param [String] key
1044
- # @param [Fixnum] start start index
1045
- # @param [Fixnum] stop stop index
1316
+ # @param [Integer] start start index
1317
+ # @param [Integer] stop stop index
1046
1318
  # @return [Array<String>]
1047
1319
  def lrange(key, start, stop)
1048
1320
  synchronize do |client|
1049
- client.call [:lrange, key, start, stop]
1321
+ client.call([:lrange, key, start, stop])
1050
1322
  end
1051
1323
  end
1052
1324
 
1053
1325
  # Remove elements from a list.
1054
1326
  #
1055
1327
  # @param [String] key
1056
- # @param [Fixnum] count number of elements to remove. Use a positive
1328
+ # @param [Integer] count number of elements to remove. Use a positive
1057
1329
  # value to remove the first `count` occurrences of `value`. A negative
1058
1330
  # value to remove the last `count` occurrences of `value`. Or zero, to
1059
1331
  # remove all occurrences of `value` from the list.
1060
1332
  # @param [String] value
1061
- # @return [Fixnum] the number of removed elements
1333
+ # @return [Integer] the number of removed elements
1062
1334
  def lrem(key, count, value)
1063
1335
  synchronize do |client|
1064
- client.call [:lrem, key, count, value]
1336
+ client.call([:lrem, key, count, value])
1065
1337
  end
1066
1338
  end
1067
1339
 
1068
1340
  # Set the value of an element in a list by its index.
1069
1341
  #
1070
1342
  # @param [String] key
1071
- # @param [Fixnum] index
1343
+ # @param [Integer] index
1072
1344
  # @param [String] value
1073
1345
  # @return [String] `OK`
1074
1346
  def lset(key, index, value)
1075
1347
  synchronize do |client|
1076
- client.call [:lset, key, index, value]
1348
+ client.call([:lset, key, index, value])
1077
1349
  end
1078
1350
  end
1079
1351
 
1080
1352
  # Trim a list to the specified range.
1081
1353
  #
1082
1354
  # @param [String] key
1083
- # @param [Fixnum] start start index
1084
- # @param [Fixnum] stop stop index
1355
+ # @param [Integer] start start index
1356
+ # @param [Integer] stop stop index
1085
1357
  # @return [String] `OK`
1086
1358
  def ltrim(key, start, stop)
1087
1359
  synchronize do |client|
1088
- client.call [:ltrim, key, start, stop]
1360
+ client.call([:ltrim, key, start, stop])
1089
1361
  end
1090
1362
  end
1091
1363
 
1092
1364
  # Get the number of members in a set.
1093
1365
  #
1094
1366
  # @param [String] key
1095
- # @return [Fixnum]
1367
+ # @return [Integer]
1096
1368
  def scard(key)
1097
1369
  synchronize do |client|
1098
- client.call [:scard, key]
1370
+ client.call([:scard, key])
1099
1371
  end
1100
1372
  end
1101
1373
 
@@ -1103,19 +1375,19 @@ class Redis
1103
1375
  #
1104
1376
  # @param [String] key
1105
1377
  # @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
1378
+ # @return [Boolean, Integer] `Boolean` when a single member is specified,
1379
+ # holding whether or not adding the member succeeded, or `Integer` when an
1108
1380
  # array of members is specified, holding the number of members that were
1109
1381
  # successfully added
1110
1382
  def sadd(key, member)
1111
1383
  synchronize do |client|
1112
- client.call [:sadd, key, member] do |reply|
1384
+ client.call([:sadd, key, member]) do |reply|
1113
1385
  if member.is_a? Array
1114
1386
  # Variadic: return integer
1115
1387
  reply
1116
1388
  else
1117
1389
  # Single argument: return boolean
1118
- _boolify.call(reply)
1390
+ Boolify.call(reply)
1119
1391
  end
1120
1392
  end
1121
1393
  end
@@ -1125,41 +1397,51 @@ class Redis
1125
1397
  #
1126
1398
  # @param [String] key
1127
1399
  # @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
1400
+ # @return [Boolean, Integer] `Boolean` when a single member is specified,
1401
+ # holding whether or not removing the member succeeded, or `Integer` when an
1130
1402
  # array of members is specified, holding the number of members that were
1131
1403
  # successfully removed
1132
1404
  def srem(key, member)
1133
1405
  synchronize do |client|
1134
- client.call [:srem, key, member] do |reply|
1406
+ client.call([:srem, key, member]) do |reply|
1135
1407
  if member.is_a? Array
1136
1408
  # Variadic: return integer
1137
1409
  reply
1138
1410
  else
1139
1411
  # Single argument: return boolean
1140
- _boolify.call(reply)
1412
+ Boolify.call(reply)
1141
1413
  end
1142
1414
  end
1143
1415
  end
1144
1416
  end
1145
1417
 
1146
- # Remove and return a random member from a set.
1418
+ # Remove and return one or more random member from a set.
1147
1419
  #
1148
1420
  # @param [String] key
1149
1421
  # @return [String]
1150
- def spop(key)
1422
+ # @param [Integer] count
1423
+ def spop(key, count = nil)
1151
1424
  synchronize do |client|
1152
- client.call [:spop, key]
1425
+ if count.nil?
1426
+ client.call([:spop, key])
1427
+ else
1428
+ client.call([:spop, key, count])
1429
+ end
1153
1430
  end
1154
1431
  end
1155
1432
 
1156
- # Get a random member from a set.
1433
+ # Get one or more random members from a set.
1157
1434
  #
1158
1435
  # @param [String] key
1436
+ # @param [Integer] count
1159
1437
  # @return [String]
1160
- def srandmember(key)
1438
+ def srandmember(key, count = nil)
1161
1439
  synchronize do |client|
1162
- client.call [:srandmember, key]
1440
+ if count.nil?
1441
+ client.call([:srandmember, key])
1442
+ else
1443
+ client.call([:srandmember, key, count])
1444
+ end
1163
1445
  end
1164
1446
  end
1165
1447
 
@@ -1171,7 +1453,7 @@ class Redis
1171
1453
  # @return [Boolean]
1172
1454
  def smove(source, destination, member)
1173
1455
  synchronize do |client|
1174
- client.call [:smove, source, destination, member], &_boolify
1456
+ client.call([:smove, source, destination, member], &Boolify)
1175
1457
  end
1176
1458
  end
1177
1459
 
@@ -1182,7 +1464,7 @@ class Redis
1182
1464
  # @return [Boolean]
1183
1465
  def sismember(key, member)
1184
1466
  synchronize do |client|
1185
- client.call [:sismember, key, member], &_boolify
1467
+ client.call([:sismember, key, member], &Boolify)
1186
1468
  end
1187
1469
  end
1188
1470
 
@@ -1192,7 +1474,7 @@ class Redis
1192
1474
  # @return [Array<String>]
1193
1475
  def smembers(key)
1194
1476
  synchronize do |client|
1195
- client.call [:smembers, key]
1477
+ client.call([:smembers, key])
1196
1478
  end
1197
1479
  end
1198
1480
 
@@ -1202,7 +1484,7 @@ class Redis
1202
1484
  # @return [Array<String>] members in the difference
1203
1485
  def sdiff(*keys)
1204
1486
  synchronize do |client|
1205
- client.call [:sdiff, *keys]
1487
+ client.call([:sdiff, *keys])
1206
1488
  end
1207
1489
  end
1208
1490
 
@@ -1210,10 +1492,10 @@ class Redis
1210
1492
  #
1211
1493
  # @param [String] destination destination key
1212
1494
  # @param [String, Array<String>] keys keys pointing to sets to subtract
1213
- # @return [Fixnum] number of elements in the resulting set
1495
+ # @return [Integer] number of elements in the resulting set
1214
1496
  def sdiffstore(destination, *keys)
1215
1497
  synchronize do |client|
1216
- client.call [:sdiffstore, destination, *keys]
1498
+ client.call([:sdiffstore, destination, *keys])
1217
1499
  end
1218
1500
  end
1219
1501
 
@@ -1223,7 +1505,7 @@ class Redis
1223
1505
  # @return [Array<String>] members in the intersection
1224
1506
  def sinter(*keys)
1225
1507
  synchronize do |client|
1226
- client.call [:sinter, *keys]
1508
+ client.call([:sinter, *keys])
1227
1509
  end
1228
1510
  end
1229
1511
 
@@ -1231,10 +1513,10 @@ class Redis
1231
1513
  #
1232
1514
  # @param [String] destination destination key
1233
1515
  # @param [String, Array<String>] keys keys pointing to sets to intersect
1234
- # @return [Fixnum] number of elements in the resulting set
1516
+ # @return [Integer] number of elements in the resulting set
1235
1517
  def sinterstore(destination, *keys)
1236
1518
  synchronize do |client|
1237
- client.call [:sinterstore, destination, *keys]
1519
+ client.call([:sinterstore, destination, *keys])
1238
1520
  end
1239
1521
  end
1240
1522
 
@@ -1244,7 +1526,7 @@ class Redis
1244
1526
  # @return [Array<String>] members in the union
1245
1527
  def sunion(*keys)
1246
1528
  synchronize do |client|
1247
- client.call [:sunion, *keys]
1529
+ client.call([:sunion, *keys])
1248
1530
  end
1249
1531
  end
1250
1532
 
@@ -1252,10 +1534,10 @@ class Redis
1252
1534
  #
1253
1535
  # @param [String] destination destination key
1254
1536
  # @param [String, Array<String>] keys keys pointing to sets to unify
1255
- # @return [Fixnum] number of elements in the resulting set
1537
+ # @return [Integer] number of elements in the resulting set
1256
1538
  def sunionstore(destination, *keys)
1257
1539
  synchronize do |client|
1258
- client.call [:sunionstore, destination, *keys]
1540
+ client.call([:sunionstore, destination, *keys])
1259
1541
  end
1260
1542
  end
1261
1543
 
@@ -1266,10 +1548,10 @@ class Redis
1266
1548
  # # => 4
1267
1549
  #
1268
1550
  # @param [String] key
1269
- # @return [Fixnum]
1551
+ # @return [Integer]
1270
1552
  def zcard(key)
1271
1553
  synchronize do |client|
1272
- client.call [:zcard, key]
1554
+ client.call([:zcard, key])
1273
1555
  end
1274
1556
  end
1275
1557
 
@@ -1285,20 +1567,39 @@ class Redis
1285
1567
  # @param [[Float, String], Array<[Float, String]>] args
1286
1568
  # - a single `[score, member]` pair
1287
1569
  # - an array of `[score, member]` pairs
1288
- #
1289
- # @return [Boolean, Fixnum]
1570
+ # @param [Hash] options
1571
+ # - `:xx => true`: Only update elements that already exist (never
1572
+ # add elements)
1573
+ # - `:nx => true`: Don't update already existing elements (always
1574
+ # add new elements)
1575
+ # - `:ch => true`: Modify the return value from the number of new
1576
+ # elements added, to the total number of elements changed (CH is an
1577
+ # abbreviation of changed); changed elements are new elements added
1578
+ # and elements already existing for which the score was updated
1579
+ # - `:incr => true`: When this option is specified ZADD acts like
1580
+ # ZINCRBY; only one score-element pair can be specified in this mode
1581
+ #
1582
+ # @return [Boolean, Integer, Float]
1290
1583
  # - `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)
1584
+ # **added** to the sorted set.
1585
+ # - `Integer` when an array of pairs is specified, holding the number of
1586
+ # pairs that were **added** to the sorted set.
1587
+ # - `Float` when option :incr is specified, holding the score of the member
1588
+ # after incrementing it.
1589
+ def zadd(key, *args, nx: nil, xx: nil, ch: nil, incr: nil)
1590
+ command = [:zadd, key]
1591
+ command << "NX" if nx
1592
+ command << "XX" if xx
1593
+ command << "CH" if ch
1594
+ command << "INCR" if incr
1595
+
1295
1596
  synchronize do |client|
1296
1597
  if args.size == 1 && args[0].is_a?(Array)
1297
- # Variadic: return integer
1298
- client.call [:zadd, key] + args[0]
1598
+ # Variadic: return float if INCR, integer if !INCR
1599
+ client.call(command + args[0], &(incr ? Floatify : nil))
1299
1600
  elsif args.size == 2
1300
- # Single pair: return boolean
1301
- client.call [:zadd, key, args[0], args[1]], &_boolify
1601
+ # Single pair: return float if INCR, boolean if !INCR
1602
+ client.call(command + args, &(incr ? Floatify : Boolify))
1302
1603
  else
1303
1604
  raise ArgumentError, "wrong number of arguments"
1304
1605
  end
@@ -1317,9 +1618,7 @@ class Redis
1317
1618
  # @return [Float] score of the member after incrementing it
1318
1619
  def zincrby(key, increment, member)
1319
1620
  synchronize do |client|
1320
- client.call [:zincrby, key, increment, member] do |reply|
1321
- Float(reply) if reply
1322
- end
1621
+ client.call([:zincrby, key, increment, member], &Floatify)
1323
1622
  end
1324
1623
  end
1325
1624
 
@@ -1335,25 +1634,109 @@ class Redis
1335
1634
  # - a single member
1336
1635
  # - an array of members
1337
1636
  #
1338
- # @return [Boolean, Fixnum]
1637
+ # @return [Boolean, Integer]
1339
1638
  # - `Boolean` when a single member is specified, holding whether or not it
1340
1639
  # was removed from the sorted set
1341
- # - `Fixnum` when an array of pairs is specified, holding the number of
1640
+ # - `Integer` when an array of pairs is specified, holding the number of
1342
1641
  # members that were removed to the sorted set
1343
1642
  def zrem(key, member)
1344
1643
  synchronize do |client|
1345
- client.call [:zrem, key, member] do |reply|
1644
+ client.call([:zrem, key, member]) do |reply|
1346
1645
  if member.is_a? Array
1347
1646
  # Variadic: return integer
1348
1647
  reply
1349
1648
  else
1350
1649
  # Single argument: return boolean
1351
- _boolify.call(reply)
1650
+ Boolify.call(reply)
1352
1651
  end
1353
1652
  end
1354
1653
  end
1355
1654
  end
1356
1655
 
1656
+ # Removes and returns up to count members with the highest scores in the sorted set stored at key.
1657
+ #
1658
+ # @example Popping a member
1659
+ # redis.zpopmax('zset')
1660
+ # #=> ['b', 2.0]
1661
+ # @example With count option
1662
+ # redis.zpopmax('zset', 2)
1663
+ # #=> [['b', 2.0], ['a', 1.0]]
1664
+ #
1665
+ # @params key [String] a key of the sorted set
1666
+ # @params count [Integer] a number of members
1667
+ #
1668
+ # @return [Array<String, Float>] element and score pair if count is not specified
1669
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1670
+ def zpopmax(key, count = nil)
1671
+ synchronize do |client|
1672
+ members = client.call([:zpopmax, key, count].compact, &FloatifyPairs)
1673
+ count.to_i > 1 ? members : members.first
1674
+ end
1675
+ end
1676
+
1677
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at key.
1678
+ #
1679
+ # @example Popping a member
1680
+ # redis.zpopmin('zset')
1681
+ # #=> ['a', 1.0]
1682
+ # @example With count option
1683
+ # redis.zpopmin('zset', 2)
1684
+ # #=> [['a', 1.0], ['b', 2.0]]
1685
+ #
1686
+ # @params key [String] a key of the sorted set
1687
+ # @params count [Integer] a number of members
1688
+ #
1689
+ # @return [Array<String, Float>] element and score pair if count is not specified
1690
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1691
+ def zpopmin(key, count = nil)
1692
+ synchronize do |client|
1693
+ members = client.call([:zpopmin, key, count].compact, &FloatifyPairs)
1694
+ count.to_i > 1 ? members : members.first
1695
+ end
1696
+ end
1697
+
1698
+ # Removes and returns up to count members with the highest scores in the sorted set stored at keys,
1699
+ # or block until one is available.
1700
+ #
1701
+ # @example Popping a member from a sorted set
1702
+ # redis.bzpopmax('zset', 1)
1703
+ # #=> ['zset', 'b', 2.0]
1704
+ # @example Popping a member from multiple sorted sets
1705
+ # redis.bzpopmax('zset1', 'zset2', 1)
1706
+ # #=> ['zset1', 'b', 2.0]
1707
+ #
1708
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1709
+ # @params timeout [Integer] the maximum number of seconds to block
1710
+ #
1711
+ # @return [Array<String, String, Float>] a touple of key, member and score
1712
+ # @return [nil] when no element could be popped and the timeout expired
1713
+ def bzpopmax(*args)
1714
+ _bpop(:bzpopmax, args) do |reply|
1715
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1716
+ end
1717
+ end
1718
+
1719
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at keys,
1720
+ # or block until one is available.
1721
+ #
1722
+ # @example Popping a member from a sorted set
1723
+ # redis.bzpopmin('zset', 1)
1724
+ # #=> ['zset', 'a', 1.0]
1725
+ # @example Popping a member from multiple sorted sets
1726
+ # redis.bzpopmin('zset1', 'zset2', 1)
1727
+ # #=> ['zset1', 'a', 1.0]
1728
+ #
1729
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1730
+ # @params timeout [Integer] the maximum number of seconds to block
1731
+ #
1732
+ # @return [Array<String, String, Float>] a touple of key, member and score
1733
+ # @return [nil] when no element could be popped and the timeout expired
1734
+ def bzpopmin(*args)
1735
+ _bpop(:bzpopmin, args) do |reply|
1736
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1737
+ end
1738
+ end
1739
+
1357
1740
  # Get the score associated with the given member in a sorted set.
1358
1741
  #
1359
1742
  # @example Get the score for member "a"
@@ -1365,9 +1748,7 @@ class Redis
1365
1748
  # @return [Float] score of the member
1366
1749
  def zscore(key, member)
1367
1750
  synchronize do |client|
1368
- client.call [:zscore, key, member] do |reply|
1369
- Float(reply) if reply
1370
- end
1751
+ client.call([:zscore, key, member], &Floatify)
1371
1752
  end
1372
1753
  end
1373
1754
 
@@ -1381,32 +1762,24 @@ class Redis
1381
1762
  # # => [["a", 32.0], ["b", 64.0]]
1382
1763
  #
1383
1764
  # @param [String] key
1384
- # @param [Fixnum] start start index
1385
- # @param [Fixnum] stop stop index
1765
+ # @param [Integer] start start index
1766
+ # @param [Integer] stop stop index
1386
1767
  # @param [Hash] options
1387
1768
  # - `:with_scores => true`: include scores in output
1388
1769
  #
1389
1770
  # @return [Array<String>, Array<[String, Float]>]
1390
1771
  # - when `:with_scores` is not specified, an array of members
1391
1772
  # - when `:with_scores` is specified, an array with `[member, score]` pairs
1392
- def zrange(key, start, stop, options = {})
1393
- args = []
1773
+ def zrange(key, start, stop, withscores: false, with_scores: withscores)
1774
+ args = [:zrange, key, start, stop]
1394
1775
 
1395
- with_scores = options[:with_scores] || options[:withscores]
1396
- args << "WITHSCORES" if with_scores
1776
+ if with_scores
1777
+ args << "WITHSCORES"
1778
+ block = FloatifyPairs
1779
+ end
1397
1780
 
1398
1781
  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
1782
+ client.call(args, &block)
1410
1783
  end
1411
1784
  end
1412
1785
 
@@ -1421,24 +1794,16 @@ class Redis
1421
1794
  # # => [["b", 64.0], ["a", 32.0]]
1422
1795
  #
1423
1796
  # @see #zrange
1424
- def zrevrange(key, start, stop, options = {})
1425
- args = []
1797
+ def zrevrange(key, start, stop, withscores: false, with_scores: withscores)
1798
+ args = [:zrevrange, key, start, stop]
1426
1799
 
1427
- with_scores = options[:with_scores] || options[:withscores]
1428
- args << "WITHSCORES" if with_scores
1800
+ if with_scores
1801
+ args << "WITHSCORES"
1802
+ block = FloatifyPairs
1803
+ end
1429
1804
 
1430
1805
  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
1806
+ client.call(args, &block)
1442
1807
  end
1443
1808
  end
1444
1809
 
@@ -1446,10 +1811,10 @@ class Redis
1446
1811
  #
1447
1812
  # @param [String] key
1448
1813
  # @param [String] member
1449
- # @return [Fixnum]
1814
+ # @return [Integer]
1450
1815
  def zrank(key, member)
1451
1816
  synchronize do |client|
1452
- client.call [:zrank, key, member]
1817
+ client.call([:zrank, key, member])
1453
1818
  end
1454
1819
  end
1455
1820
 
@@ -1458,10 +1823,10 @@ class Redis
1458
1823
  #
1459
1824
  # @param [String] key
1460
1825
  # @param [String] member
1461
- # @return [Fixnum]
1826
+ # @return [Integer]
1462
1827
  def zrevrank(key, member)
1463
1828
  synchronize do |client|
1464
- client.call [:zrevrank, key, member]
1829
+ client.call([:zrevrank, key, member])
1465
1830
  end
1466
1831
  end
1467
1832
 
@@ -1475,12 +1840,94 @@ class Redis
1475
1840
  # # => 5
1476
1841
  #
1477
1842
  # @param [String] key
1478
- # @param [Fixnum] start start index
1479
- # @param [Fixnum] stop stop index
1480
- # @return [Fixnum] number of members that were removed
1843
+ # @param [Integer] start start index
1844
+ # @param [Integer] stop stop index
1845
+ # @return [Integer] number of members that were removed
1481
1846
  def zremrangebyrank(key, start, stop)
1482
1847
  synchronize do |client|
1483
- client.call [:zremrangebyrank, key, start, stop]
1848
+ client.call([:zremrangebyrank, key, start, stop])
1849
+ end
1850
+ end
1851
+
1852
+ # Count the members, with the same score in a sorted set, within the given lexicographical range.
1853
+ #
1854
+ # @example Count members matching a
1855
+ # redis.zlexcount("zset", "[a", "[a\xff")
1856
+ # # => 1
1857
+ # @example Count members matching a-z
1858
+ # redis.zlexcount("zset", "[a", "[z\xff")
1859
+ # # => 26
1860
+ #
1861
+ # @param [String] key
1862
+ # @param [String] min
1863
+ # - inclusive minimum is specified by prefixing `(`
1864
+ # - exclusive minimum is specified by prefixing `[`
1865
+ # @param [String] max
1866
+ # - inclusive maximum is specified by prefixing `(`
1867
+ # - exclusive maximum is specified by prefixing `[`
1868
+ #
1869
+ # @return [Integer] number of members within the specified lexicographical range
1870
+ def zlexcount(key, min, max)
1871
+ synchronize do |client|
1872
+ client.call([:zlexcount, key, min, max])
1873
+ end
1874
+ end
1875
+
1876
+ # Return a range of members with the same score in a sorted set, by lexicographical ordering
1877
+ #
1878
+ # @example Retrieve members matching a
1879
+ # redis.zrangebylex("zset", "[a", "[a\xff")
1880
+ # # => ["aaren", "aarika", "abagael", "abby"]
1881
+ # @example Retrieve the first 2 members matching a
1882
+ # redis.zrangebylex("zset", "[a", "[a\xff", :limit => [0, 2])
1883
+ # # => ["aaren", "aarika"]
1884
+ #
1885
+ # @param [String] key
1886
+ # @param [String] min
1887
+ # - inclusive minimum is specified by prefixing `(`
1888
+ # - exclusive minimum is specified by prefixing `[`
1889
+ # @param [String] max
1890
+ # - inclusive maximum is specified by prefixing `(`
1891
+ # - exclusive maximum is specified by prefixing `[`
1892
+ # @param [Hash] options
1893
+ # - `:limit => [offset, count]`: skip `offset` members, return a maximum of
1894
+ # `count` members
1895
+ #
1896
+ # @return [Array<String>, Array<[String, Float]>]
1897
+ def zrangebylex(key, min, max, limit: nil)
1898
+ args = [:zrangebylex, key, min, max]
1899
+
1900
+ if limit
1901
+ args << "LIMIT"
1902
+ args.concat(limit)
1903
+ end
1904
+
1905
+ synchronize do |client|
1906
+ client.call(args)
1907
+ end
1908
+ end
1909
+
1910
+ # Return a range of members with the same score in a sorted set, by reversed lexicographical ordering.
1911
+ # Apart from the reversed ordering, #zrevrangebylex is similar to #zrangebylex.
1912
+ #
1913
+ # @example Retrieve members matching a
1914
+ # redis.zrevrangebylex("zset", "[a", "[a\xff")
1915
+ # # => ["abbygail", "abby", "abagael", "aaren"]
1916
+ # @example Retrieve the last 2 members matching a
1917
+ # redis.zrevrangebylex("zset", "[a", "[a\xff", :limit => [0, 2])
1918
+ # # => ["abbygail", "abby"]
1919
+ #
1920
+ # @see #zrangebylex
1921
+ def zrevrangebylex(key, max, min, limit: nil)
1922
+ args = [:zrevrangebylex, key, max, min]
1923
+
1924
+ if limit
1925
+ args << "LIMIT"
1926
+ args.concat(limit)
1927
+ end
1928
+
1929
+ synchronize do |client|
1930
+ client.call(args)
1484
1931
  end
1485
1932
  end
1486
1933
 
@@ -1511,27 +1958,21 @@ class Redis
1511
1958
  # @return [Array<String>, Array<[String, Float]>]
1512
1959
  # - when `:with_scores` is not specified, an array of members
1513
1960
  # - when `:with_scores` is specified, an array with `[member, score]` pairs
1514
- def zrangebyscore(key, min, max, options = {})
1515
- args = []
1961
+ def zrangebyscore(key, min, max, withscores: false, with_scores: withscores, limit: nil)
1962
+ args = [:zrangebyscore, key, min, max]
1516
1963
 
1517
- with_scores = options[:with_scores] || options[:withscores]
1518
- args.concat ["WITHSCORES"] if with_scores
1964
+ if with_scores
1965
+ args << "WITHSCORES"
1966
+ block = FloatifyPairs
1967
+ end
1519
1968
 
1520
- limit = options[:limit]
1521
- args.concat ["LIMIT", *limit] if limit
1969
+ if limit
1970
+ args << "LIMIT"
1971
+ args.concat(limit)
1972
+ end
1522
1973
 
1523
1974
  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
1975
+ client.call(args, &block)
1535
1976
  end
1536
1977
  end
1537
1978
 
@@ -1549,27 +1990,21 @@ class Redis
1549
1990
  # # => [["b", 64.0], ["a", 32.0]]
1550
1991
  #
1551
1992
  # @see #zrangebyscore
1552
- def zrevrangebyscore(key, max, min, options = {})
1553
- args = []
1993
+ def zrevrangebyscore(key, max, min, withscores: false, with_scores: withscores, limit: nil)
1994
+ args = [:zrevrangebyscore, key, max, min]
1554
1995
 
1555
- with_scores = options[:with_scores] || options[:withscores]
1556
- args.concat ["WITHSCORES"] if with_scores
1996
+ if with_scores
1997
+ args << "WITHSCORES"
1998
+ block = FloatifyPairs
1999
+ end
1557
2000
 
1558
- limit = options[:limit]
1559
- args.concat ["LIMIT", *limit] if limit
2001
+ if limit
2002
+ args << "LIMIT"
2003
+ args.concat(limit)
2004
+ end
1560
2005
 
1561
2006
  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
2007
+ client.call(args, &block)
1573
2008
  end
1574
2009
  end
1575
2010
 
@@ -1589,10 +2024,10 @@ class Redis
1589
2024
  # @param [String] max
1590
2025
  # - inclusive maximum score is specified verbatim
1591
2026
  # - exclusive maximum score is specified by prefixing `(`
1592
- # @return [Fixnum] number of members that were removed
2027
+ # @return [Integer] number of members that were removed
1593
2028
  def zremrangebyscore(key, min, max)
1594
2029
  synchronize do |client|
1595
- client.call [:zremrangebyscore, key, min, max]
2030
+ client.call([:zremrangebyscore, key, min, max])
1596
2031
  end
1597
2032
  end
1598
2033
 
@@ -1612,10 +2047,10 @@ class Redis
1612
2047
  # @param [String] max
1613
2048
  # - inclusive maximum score is specified verbatim
1614
2049
  # - exclusive maximum score is specified by prefixing `(`
1615
- # @return [Fixnum] number of members in within the specified range
2050
+ # @return [Integer] number of members in within the specified range
1616
2051
  def zcount(key, min, max)
1617
2052
  synchronize do |client|
1618
- client.call [:zcount, key, min, max]
2053
+ client.call([:zcount, key, min, max])
1619
2054
  end
1620
2055
  end
1621
2056
 
@@ -1632,18 +2067,19 @@ class Redis
1632
2067
  # - `:weights => [Float, Float, ...]`: weights to associate with source
1633
2068
  # sorted sets
1634
2069
  # - `: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 = []
2070
+ # @return [Integer] number of elements in the resulting sorted set
2071
+ def zinterstore(destination, keys, weights: nil, aggregate: nil)
2072
+ args = [:zinterstore, destination, keys.size, *keys]
1638
2073
 
1639
- weights = options[:weights]
1640
- args.concat ["WEIGHTS", *weights] if weights
2074
+ if weights
2075
+ args << "WEIGHTS"
2076
+ args.concat(weights)
2077
+ end
1641
2078
 
1642
- aggregate = options[:aggregate]
1643
- args.concat ["AGGREGATE", aggregate] if aggregate
2079
+ args << "AGGREGATE" << aggregate if aggregate
1644
2080
 
1645
2081
  synchronize do |client|
1646
- client.call [:zinterstore, destination, keys.size, *(keys + args)]
2082
+ client.call(args)
1647
2083
  end
1648
2084
  end
1649
2085
 
@@ -1659,40 +2095,46 @@ class Redis
1659
2095
  # - `:weights => [Float, Float, ...]`: weights to associate with source
1660
2096
  # sorted sets
1661
2097
  # - `: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 = []
2098
+ # @return [Integer] number of elements in the resulting sorted set
2099
+ def zunionstore(destination, keys, weights: nil, aggregate: nil)
2100
+ args = [:zunionstore, destination, keys.size, *keys]
1665
2101
 
1666
- weights = options[:weights]
1667
- args.concat ["WEIGHTS", *weights] if weights
2102
+ if weights
2103
+ args << "WEIGHTS"
2104
+ args.concat(weights)
2105
+ end
1668
2106
 
1669
- aggregate = options[:aggregate]
1670
- args.concat ["AGGREGATE", aggregate] if aggregate
2107
+ args << "AGGREGATE" << aggregate if aggregate
1671
2108
 
1672
2109
  synchronize do |client|
1673
- client.call [:zunionstore, destination, keys.size, *(keys + args)]
2110
+ client.call(args)
1674
2111
  end
1675
2112
  end
1676
2113
 
1677
2114
  # Get the number of fields in a hash.
1678
2115
  #
1679
2116
  # @param [String] key
1680
- # @return [Fixnum] number of fields in the hash
2117
+ # @return [Integer] number of fields in the hash
1681
2118
  def hlen(key)
1682
2119
  synchronize do |client|
1683
- client.call [:hlen, key]
2120
+ client.call([:hlen, key])
1684
2121
  end
1685
2122
  end
1686
2123
 
1687
- # Set the string value of a hash field.
2124
+ # Set one or more hash values.
2125
+ #
2126
+ # @example
2127
+ # redis.hset("hash", "f1", "v1", "f2", "v2") # => 2
2128
+ # redis.hset("hash", { "f1" => "v1", "f2" => "v2" }) # => 2
1688
2129
  #
1689
2130
  # @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)
2131
+ # @param [Array<String> | Hash<String, String>] attrs array or hash of fields and values
2132
+ # @return [Integer] The number of fields that were added to the hash
2133
+ def hset(key, *attrs)
2134
+ attrs = attrs.first.flatten if attrs.size == 1 && attrs.first.is_a?(Hash)
2135
+
1694
2136
  synchronize do |client|
1695
- client.call [:hset, key, field, value], &_boolify
2137
+ client.call([:hset, key, *attrs])
1696
2138
  end
1697
2139
  end
1698
2140
 
@@ -1704,7 +2146,7 @@ class Redis
1704
2146
  # @return [Boolean] whether or not the field was **added** to the hash
1705
2147
  def hsetnx(key, field, value)
1706
2148
  synchronize do |client|
1707
- client.call [:hsetnx, key, field, value], &_boolify
2149
+ client.call([:hsetnx, key, field, value], &Boolify)
1708
2150
  end
1709
2151
  end
1710
2152
 
@@ -1716,28 +2158,28 @@ class Redis
1716
2158
  #
1717
2159
  # @param [String] key
1718
2160
  # @param [Array<String>] attrs array of fields and values
1719
- # @return `"OK"`
2161
+ # @return [String] `"OK"`
1720
2162
  #
1721
2163
  # @see #mapped_hmset
1722
2164
  def hmset(key, *attrs)
1723
2165
  synchronize do |client|
1724
- client.call [:hmset, key, *attrs]
2166
+ client.call([:hmset, key] + attrs)
1725
2167
  end
1726
2168
  end
1727
2169
 
1728
2170
  # Set one or more hash values.
1729
2171
  #
1730
2172
  # @example
1731
- # redis.hmset("hash", { "f1" => "v1", "f2" => "v2" })
2173
+ # redis.mapped_hmset("hash", { "f1" => "v1", "f2" => "v2" })
1732
2174
  # # => "OK"
1733
2175
  #
1734
2176
  # @param [String] key
1735
- # @param [Hash] hash fields mapping to values
1736
- # @return `"OK"`
2177
+ # @param [Hash] hash a non-empty hash with fields mapping to values
2178
+ # @return [String] `"OK"`
1737
2179
  #
1738
2180
  # @see #hmset
1739
2181
  def mapped_hmset(key, hash)
1740
- hmset(key, *hash.to_a.flatten)
2182
+ hmset(key, hash.to_a.flatten)
1741
2183
  end
1742
2184
 
1743
2185
  # Get the value of a hash field.
@@ -1747,7 +2189,7 @@ class Redis
1747
2189
  # @return [String]
1748
2190
  def hget(key, field)
1749
2191
  synchronize do |client|
1750
- client.call [:hget, key, field]
2192
+ client.call([:hget, key, field])
1751
2193
  end
1752
2194
  end
1753
2195
 
@@ -1764,14 +2206,14 @@ class Redis
1764
2206
  # @see #mapped_hmget
1765
2207
  def hmget(key, *fields, &blk)
1766
2208
  synchronize do |client|
1767
- client.call [:hmget, key, *fields], &blk
2209
+ client.call([:hmget, key] + fields, &blk)
1768
2210
  end
1769
2211
  end
1770
2212
 
1771
2213
  # Get the values of all the given hash fields.
1772
2214
  #
1773
2215
  # @example
1774
- # redis.hmget("hash", "f1", "f2")
2216
+ # redis.mapped_hmget("hash", "f1", "f2")
1775
2217
  # # => { "f1" => "v1", "f2" => "v2" }
1776
2218
  #
1777
2219
  # @param [String] key
@@ -1781,12 +2223,8 @@ class Redis
1781
2223
  # @see #hmget
1782
2224
  def mapped_hmget(key, *fields)
1783
2225
  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
2226
+ if reply.is_a?(Array)
2227
+ Hash[fields.zip(reply)]
1790
2228
  else
1791
2229
  reply
1792
2230
  end
@@ -1797,10 +2235,10 @@ class Redis
1797
2235
  #
1798
2236
  # @param [String] key
1799
2237
  # @param [String, Array<String>] field
1800
- # @return [Fixnum] the number of fields that were removed from the hash
1801
- def hdel(key, field)
2238
+ # @return [Integer] the number of fields that were removed from the hash
2239
+ def hdel(key, *fields)
1802
2240
  synchronize do |client|
1803
- client.call [:hdel, key, field]
2241
+ client.call([:hdel, key, *fields])
1804
2242
  end
1805
2243
  end
1806
2244
 
@@ -1811,7 +2249,7 @@ class Redis
1811
2249
  # @return [Boolean] whether or not the field exists in the hash
1812
2250
  def hexists(key, field)
1813
2251
  synchronize do |client|
1814
- client.call [:hexists, key, field], &_boolify
2252
+ client.call([:hexists, key, field], &Boolify)
1815
2253
  end
1816
2254
  end
1817
2255
 
@@ -1819,11 +2257,11 @@ class Redis
1819
2257
  #
1820
2258
  # @param [String] key
1821
2259
  # @param [String] field
1822
- # @param [Fixnum] increment
1823
- # @return [Fixnum] value of the field after incrementing it
2260
+ # @param [Integer] increment
2261
+ # @return [Integer] value of the field after incrementing it
1824
2262
  def hincrby(key, field, increment)
1825
2263
  synchronize do |client|
1826
- client.call [:hincrby, key, field, increment]
2264
+ client.call([:hincrby, key, field, increment])
1827
2265
  end
1828
2266
  end
1829
2267
 
@@ -1835,9 +2273,7 @@ class Redis
1835
2273
  # @return [Float] value of the field after incrementing it
1836
2274
  def hincrbyfloat(key, field, increment)
1837
2275
  synchronize do |client|
1838
- client.call [:hincrbyfloat, key, field, increment] do |reply|
1839
- Float(reply) if reply
1840
- end
2276
+ client.call([:hincrbyfloat, key, field, increment], &Floatify)
1841
2277
  end
1842
2278
  end
1843
2279
 
@@ -1847,7 +2283,7 @@ class Redis
1847
2283
  # @return [Array<String>]
1848
2284
  def hkeys(key)
1849
2285
  synchronize do |client|
1850
- client.call [:hkeys, key]
2286
+ client.call([:hkeys, key])
1851
2287
  end
1852
2288
  end
1853
2289
 
@@ -1857,7 +2293,7 @@ class Redis
1857
2293
  # @return [Array<String>]
1858
2294
  def hvals(key)
1859
2295
  synchronize do |client|
1860
- client.call [:hvals, key]
2296
+ client.call([:hvals, key])
1861
2297
  end
1862
2298
  end
1863
2299
 
@@ -1867,53 +2303,79 @@ class Redis
1867
2303
  # @return [Hash<String, String>]
1868
2304
  def hgetall(key)
1869
2305
  synchronize do |client|
1870
- client.call [:hgetall, key], &_hashify
2306
+ client.call([:hgetall, key], &Hashify)
1871
2307
  end
1872
2308
  end
1873
2309
 
1874
2310
  # Post a message to a channel.
1875
2311
  def publish(channel, message)
1876
2312
  synchronize do |client|
1877
- client.call [:publish, channel, message]
2313
+ client.call([:publish, channel, message])
1878
2314
  end
1879
2315
  end
1880
2316
 
1881
2317
  def subscribed?
1882
2318
  synchronize do |client|
1883
- client.kind_of? SubscribedClient
2319
+ client.is_a? SubscribedClient
1884
2320
  end
1885
2321
  end
1886
2322
 
1887
2323
  # Listen for messages published to the given channels.
1888
2324
  def subscribe(*channels, &block)
1889
- synchronize do |client|
1890
- _subscription(:subscribe, channels, block)
2325
+ synchronize do |_client|
2326
+ _subscription(:subscribe, 0, channels, block)
2327
+ end
2328
+ end
2329
+
2330
+ # Listen for messages published to the given channels. Throw a timeout error
2331
+ # if there is no messages for a timeout period.
2332
+ def subscribe_with_timeout(timeout, *channels, &block)
2333
+ synchronize do |_client|
2334
+ _subscription(:subscribe_with_timeout, timeout, channels, block)
1891
2335
  end
1892
2336
  end
1893
2337
 
1894
2338
  # Stop listening for messages posted to the given channels.
1895
2339
  def unsubscribe(*channels)
1896
2340
  synchronize do |client|
1897
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2341
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2342
+
1898
2343
  client.unsubscribe(*channels)
1899
2344
  end
1900
2345
  end
1901
2346
 
1902
2347
  # Listen for messages published to channels matching the given patterns.
1903
2348
  def psubscribe(*channels, &block)
1904
- synchronize do |client|
1905
- _subscription(:psubscribe, channels, block)
2349
+ synchronize do |_client|
2350
+ _subscription(:psubscribe, 0, channels, block)
2351
+ end
2352
+ end
2353
+
2354
+ # Listen for messages published to channels matching the given patterns.
2355
+ # Throw a timeout error if there is no messages for a timeout period.
2356
+ def psubscribe_with_timeout(timeout, *channels, &block)
2357
+ synchronize do |_client|
2358
+ _subscription(:psubscribe_with_timeout, timeout, channels, block)
1906
2359
  end
1907
2360
  end
1908
2361
 
1909
2362
  # Stop listening for messages posted to channels matching the given patterns.
1910
2363
  def punsubscribe(*channels)
1911
2364
  synchronize do |client|
1912
- raise RuntimeError, "Can't unsubscribe if not subscribed." unless subscribed?
2365
+ raise "Can't unsubscribe if not subscribed." unless subscribed?
2366
+
1913
2367
  client.punsubscribe(*channels)
1914
2368
  end
1915
2369
  end
1916
2370
 
2371
+ # Inspect the state of the Pub/Sub subsystem.
2372
+ # Possible subcommands: channels, numsub, numpat.
2373
+ def pubsub(subcommand, *args)
2374
+ synchronize do |client|
2375
+ client.call([:pubsub, subcommand] + args)
2376
+ end
2377
+ end
2378
+
1917
2379
  # Watch the given keys to determine execution of the MULTI/EXEC block.
1918
2380
  #
1919
2381
  # Using a block is optional, but is necessary for thread-safety.
@@ -1946,17 +2408,19 @@ class Redis
1946
2408
  # @see #multi
1947
2409
  def watch(*keys)
1948
2410
  synchronize do |client|
1949
- client.call [:watch, *keys]
2411
+ res = client.call([:watch, *keys])
1950
2412
 
1951
2413
  if block_given?
1952
2414
  begin
1953
- yield
2415
+ yield(self)
1954
2416
  rescue ConnectionError
1955
2417
  raise
1956
2418
  rescue StandardError
1957
2419
  unwatch
1958
2420
  raise
1959
2421
  end
2422
+ else
2423
+ res
1960
2424
  end
1961
2425
  end
1962
2426
  end
@@ -1969,14 +2433,15 @@ class Redis
1969
2433
  # @see #multi
1970
2434
  def unwatch
1971
2435
  synchronize do |client|
1972
- client.call [:unwatch]
2436
+ client.call([:unwatch])
1973
2437
  end
1974
2438
  end
1975
2439
 
1976
2440
  def pipelined
1977
- synchronize do |client|
2441
+ synchronize do |_client|
1978
2442
  begin
1979
- original, @client = @client, Pipeline.new
2443
+ pipeline = Pipeline.new(@client)
2444
+ original, @client = @client, pipeline
1980
2445
  yield(self)
1981
2446
  original.call_pipeline(@client)
1982
2447
  ensure
@@ -2018,10 +2483,10 @@ class Redis
2018
2483
  def multi
2019
2484
  synchronize do |client|
2020
2485
  if !block_given?
2021
- client.call [:multi]
2486
+ client.call([:multi])
2022
2487
  else
2023
2488
  begin
2024
- pipeline = Pipeline::Multi.new
2489
+ pipeline = Pipeline::Multi.new(@client)
2025
2490
  original, @client = @client, pipeline
2026
2491
  yield(self)
2027
2492
  original.call_pipeline(pipeline)
@@ -2044,7 +2509,7 @@ class Redis
2044
2509
  # @see #discard
2045
2510
  def exec
2046
2511
  synchronize do |client|
2047
- client.call [:exec]
2512
+ client.call([:exec])
2048
2513
  end
2049
2514
  end
2050
2515
 
@@ -2052,13 +2517,13 @@ class Redis
2052
2517
  #
2053
2518
  # Only call this method when `#multi` was called **without** a block.
2054
2519
  #
2055
- # @return `"OK"`
2520
+ # @return [String] `"OK"`
2056
2521
  #
2057
2522
  # @see #multi
2058
2523
  # @see #exec
2059
2524
  def discard
2060
2525
  synchronize do |client|
2061
- client.call [:discard]
2526
+ client.call([:discard])
2062
2527
  end
2063
2528
  end
2064
2529
 
@@ -2093,8 +2558,8 @@ class Redis
2093
2558
  synchronize do |client|
2094
2559
  arg = args.first
2095
2560
 
2096
- client.call [:script, :exists, arg] do |reply|
2097
- reply = reply.map { |r| _boolify.call(r) }
2561
+ client.call([:script, :exists, arg]) do |reply|
2562
+ reply = reply.map { |r| Boolify.call(r) }
2098
2563
 
2099
2564
  if arg.is_a?(Array)
2100
2565
  reply
@@ -2105,7 +2570,7 @@ class Redis
2105
2570
  end
2106
2571
  else
2107
2572
  synchronize do |client|
2108
- client.call [:script, subcommand] + args
2573
+ client.call([:script, subcommand] + args)
2109
2574
  end
2110
2575
  end
2111
2576
  end
@@ -2119,7 +2584,7 @@ class Redis
2119
2584
  argv = args.shift || options[:argv] || []
2120
2585
 
2121
2586
  synchronize do |client|
2122
- client.call [cmd, script, keys.length] + keys + argv
2587
+ client.call([cmd, script, keys.length] + keys + argv)
2123
2588
  end
2124
2589
  end
2125
2590
 
@@ -2173,60 +2638,904 @@ class Redis
2173
2638
  _eval(:evalsha, args)
2174
2639
  end
2175
2640
 
2176
- def id
2641
+ def _scan(command, cursor, args, match: nil, count: nil, &block)
2642
+ # SSCAN/ZSCAN/HSCAN already prepend the key to +args+.
2643
+
2644
+ args << cursor
2645
+ args << "MATCH" << match if match
2646
+ args << "COUNT" << count if count
2647
+
2177
2648
  synchronize do |client|
2178
- client.id
2649
+ client.call([command] + args, &block)
2179
2650
  end
2180
2651
  end
2181
2652
 
2182
- def inspect
2653
+ # Scan the keyspace
2654
+ #
2655
+ # @example Retrieve the first batch of keys
2656
+ # redis.scan(0)
2657
+ # # => ["4", ["key:21", "key:47", "key:42"]]
2658
+ # @example Retrieve a batch of keys matching a pattern
2659
+ # redis.scan(4, :match => "key:1?")
2660
+ # # => ["92", ["key:13", "key:18"]]
2661
+ #
2662
+ # @param [String, Integer] cursor the cursor of the iteration
2663
+ # @param [Hash] options
2664
+ # - `:match => String`: only return keys matching the pattern
2665
+ # - `:count => Integer`: return count keys at most per iteration
2666
+ #
2667
+ # @return [String, Array<String>] the next cursor and all found keys
2668
+ def scan(cursor, **options)
2669
+ _scan(:scan, cursor, [], **options)
2670
+ end
2671
+
2672
+ # Scan the keyspace
2673
+ #
2674
+ # @example Retrieve all of the keys (with possible duplicates)
2675
+ # redis.scan_each.to_a
2676
+ # # => ["key:21", "key:47", "key:42"]
2677
+ # @example Execute block for each key matching a pattern
2678
+ # redis.scan_each(:match => "key:1?") {|key| puts key}
2679
+ # # => key:13
2680
+ # # => key:18
2681
+ #
2682
+ # @param [Hash] options
2683
+ # - `:match => String`: only return keys matching the pattern
2684
+ # - `:count => Integer`: return count keys at most per iteration
2685
+ #
2686
+ # @return [Enumerator] an enumerator for all found keys
2687
+ def scan_each(**options, &block)
2688
+ return to_enum(:scan_each, **options) unless block_given?
2689
+
2690
+ cursor = 0
2691
+ loop do
2692
+ cursor, keys = scan(cursor, **options)
2693
+ keys.each(&block)
2694
+ break if cursor == "0"
2695
+ end
2696
+ end
2697
+
2698
+ # Scan a hash
2699
+ #
2700
+ # @example Retrieve the first batch of key/value pairs in a hash
2701
+ # redis.hscan("hash", 0)
2702
+ #
2703
+ # @param [String, Integer] cursor the cursor of the iteration
2704
+ # @param [Hash] options
2705
+ # - `:match => String`: only return keys matching the pattern
2706
+ # - `:count => Integer`: return count keys at most per iteration
2707
+ #
2708
+ # @return [String, Array<[String, String]>] the next cursor and all found keys
2709
+ def hscan(key, cursor, **options)
2710
+ _scan(:hscan, cursor, [key], **options) do |reply|
2711
+ [reply[0], reply[1].each_slice(2).to_a]
2712
+ end
2713
+ end
2714
+
2715
+ # Scan a hash
2716
+ #
2717
+ # @example Retrieve all of the key/value pairs in a hash
2718
+ # redis.hscan_each("hash").to_a
2719
+ # # => [["key70", "70"], ["key80", "80"]]
2720
+ #
2721
+ # @param [Hash] options
2722
+ # - `:match => String`: only return keys matching the pattern
2723
+ # - `:count => Integer`: return count keys at most per iteration
2724
+ #
2725
+ # @return [Enumerator] an enumerator for all found keys
2726
+ def hscan_each(key, **options, &block)
2727
+ return to_enum(:hscan_each, key, **options) unless block_given?
2728
+
2729
+ cursor = 0
2730
+ loop do
2731
+ cursor, values = hscan(key, cursor, **options)
2732
+ values.each(&block)
2733
+ break if cursor == "0"
2734
+ end
2735
+ end
2736
+
2737
+ # Scan a sorted set
2738
+ #
2739
+ # @example Retrieve the first batch of key/value pairs in a hash
2740
+ # redis.zscan("zset", 0)
2741
+ #
2742
+ # @param [String, Integer] cursor the cursor of the iteration
2743
+ # @param [Hash] options
2744
+ # - `:match => String`: only return keys matching the pattern
2745
+ # - `:count => Integer`: return count keys at most per iteration
2746
+ #
2747
+ # @return [String, Array<[String, Float]>] the next cursor and all found
2748
+ # members and scores
2749
+ def zscan(key, cursor, **options)
2750
+ _scan(:zscan, cursor, [key], **options) do |reply|
2751
+ [reply[0], FloatifyPairs.call(reply[1])]
2752
+ end
2753
+ end
2754
+
2755
+ # Scan a sorted set
2756
+ #
2757
+ # @example Retrieve all of the members/scores in a sorted set
2758
+ # redis.zscan_each("zset").to_a
2759
+ # # => [["key70", "70"], ["key80", "80"]]
2760
+ #
2761
+ # @param [Hash] options
2762
+ # - `:match => String`: only return keys matching the pattern
2763
+ # - `:count => Integer`: return count keys at most per iteration
2764
+ #
2765
+ # @return [Enumerator] an enumerator for all found scores and members
2766
+ def zscan_each(key, **options, &block)
2767
+ return to_enum(:zscan_each, key, **options) unless block_given?
2768
+
2769
+ cursor = 0
2770
+ loop do
2771
+ cursor, values = zscan(key, cursor, **options)
2772
+ values.each(&block)
2773
+ break if cursor == "0"
2774
+ end
2775
+ end
2776
+
2777
+ # Scan a set
2778
+ #
2779
+ # @example Retrieve the first batch of keys in a set
2780
+ # redis.sscan("set", 0)
2781
+ #
2782
+ # @param [String, Integer] cursor the cursor of the iteration
2783
+ # @param [Hash] options
2784
+ # - `:match => String`: only return keys matching the pattern
2785
+ # - `:count => Integer`: return count keys at most per iteration
2786
+ #
2787
+ # @return [String, Array<String>] the next cursor and all found members
2788
+ def sscan(key, cursor, **options)
2789
+ _scan(:sscan, cursor, [key], **options)
2790
+ end
2791
+
2792
+ # Scan a set
2793
+ #
2794
+ # @example Retrieve all of the keys in a set
2795
+ # redis.sscan_each("set").to_a
2796
+ # # => ["key1", "key2", "key3"]
2797
+ #
2798
+ # @param [Hash] options
2799
+ # - `:match => String`: only return keys matching the pattern
2800
+ # - `:count => Integer`: return count keys at most per iteration
2801
+ #
2802
+ # @return [Enumerator] an enumerator for all keys in the set
2803
+ def sscan_each(key, **options, &block)
2804
+ return to_enum(:sscan_each, key, **options) unless block_given?
2805
+
2806
+ cursor = 0
2807
+ loop do
2808
+ cursor, keys = sscan(key, cursor, **options)
2809
+ keys.each(&block)
2810
+ break if cursor == "0"
2811
+ end
2812
+ end
2813
+
2814
+ # Add one or more members to a HyperLogLog structure.
2815
+ #
2816
+ # @param [String] key
2817
+ # @param [String, Array<String>] member one member, or array of members
2818
+ # @return [Boolean] true if at least 1 HyperLogLog internal register was altered. false otherwise.
2819
+ def pfadd(key, member)
2183
2820
  synchronize do |client|
2184
- "#<Redis client v#{Redis::VERSION} for #{client.id}>"
2821
+ client.call([:pfadd, key, member], &Boolify)
2185
2822
  end
2186
2823
  end
2187
2824
 
2188
- def method_missing(command, *args)
2825
+ # Get the approximate cardinality of members added to HyperLogLog structure.
2826
+ #
2827
+ # If called with multiple keys, returns the approximate cardinality of the
2828
+ # union of the HyperLogLogs contained in the keys.
2829
+ #
2830
+ # @param [String, Array<String>] keys
2831
+ # @return [Integer]
2832
+ def pfcount(*keys)
2189
2833
  synchronize do |client|
2190
- client.call [command, *args]
2834
+ client.call([:pfcount] + keys)
2191
2835
  end
2192
2836
  end
2193
2837
 
2194
- private
2838
+ # Merge multiple HyperLogLog values into an unique value that will approximate the cardinality of the union of
2839
+ # the observed Sets of the source HyperLogLog structures.
2840
+ #
2841
+ # @param [String] dest_key destination key
2842
+ # @param [String, Array<String>] source_key source key, or array of keys
2843
+ # @return [Boolean]
2844
+ def pfmerge(dest_key, *source_key)
2845
+ synchronize do |client|
2846
+ client.call([:pfmerge, dest_key, *source_key], &BoolifySet)
2847
+ end
2848
+ end
2849
+
2850
+ # Adds the specified geospatial items (latitude, longitude, name) to the specified key
2851
+ #
2852
+ # @param [String] key
2853
+ # @param [Array] member arguemnts for member or members: longitude, latitude, name
2854
+ # @return [Integer] number of elements added to the sorted set
2855
+ def geoadd(key, *member)
2856
+ synchronize do |client|
2857
+ client.call([:geoadd, key, *member])
2858
+ end
2859
+ end
2860
+
2861
+ # Returns geohash string representing position for specified members of the specified key.
2862
+ #
2863
+ # @param [String] key
2864
+ # @param [String, Array<String>] member one member or array of members
2865
+ # @return [Array<String, nil>] returns array containg geohash string if member is present, nil otherwise
2866
+ def geohash(key, member)
2867
+ synchronize do |client|
2868
+ client.call([:geohash, key, member])
2869
+ end
2870
+ end
2871
+
2872
+ # Query a sorted set representing a geospatial index to fetch members matching a
2873
+ # given maximum distance from a point
2874
+ #
2875
+ # @param [Array] args key, longitude, latitude, radius, unit(m|km|ft|mi)
2876
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest
2877
+ # or the farthest to the nearest relative to the center
2878
+ # @param [Integer] count limit the results to the first N matching items
2879
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2880
+ # @return [Array<String>] may be changed with `options`
2881
+
2882
+ def georadius(*args, **geoptions)
2883
+ geoarguments = _geoarguments(*args, **geoptions)
2884
+
2885
+ synchronize do |client|
2886
+ client.call([:georadius, *geoarguments])
2887
+ end
2888
+ end
2889
+
2890
+ # Query a sorted set representing a geospatial index to fetch members matching a
2891
+ # given maximum distance from an already existing member
2892
+ #
2893
+ # @param [Array] args key, member, radius, unit(m|km|ft|mi)
2894
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest
2895
+ # to the nearest relative to the center
2896
+ # @param [Integer] count limit the results to the first N matching items
2897
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2898
+ # @return [Array<String>] may be changed with `options`
2899
+
2900
+ def georadiusbymember(*args, **geoptions)
2901
+ geoarguments = _geoarguments(*args, **geoptions)
2902
+
2903
+ synchronize do |client|
2904
+ client.call([:georadiusbymember, *geoarguments])
2905
+ end
2906
+ end
2907
+
2908
+ # Returns longitude and latitude of members of a geospatial index
2909
+ #
2910
+ # @param [String] key
2911
+ # @param [String, Array<String>] member one member or array of members
2912
+ # @return [Array<Array<String>, nil>] returns array of elements, where each
2913
+ # element is either array of longitude and latitude or nil
2914
+ def geopos(key, member)
2915
+ synchronize do |client|
2916
+ client.call([:geopos, key, member])
2917
+ end
2918
+ end
2919
+
2920
+ # Returns the distance between two members of a geospatial index
2921
+ #
2922
+ # @param [String ]key
2923
+ # @param [Array<String>] members
2924
+ # @param ['m', 'km', 'mi', 'ft'] unit
2925
+ # @return [String, nil] returns distance in spefied unit if both members present, nil otherwise.
2926
+ def geodist(key, member1, member2, unit = 'm')
2927
+ synchronize do |client|
2928
+ client.call([:geodist, key, member1, member2, unit])
2929
+ end
2930
+ end
2931
+
2932
+ # Returns the stream information each subcommand.
2933
+ #
2934
+ # @example stream
2935
+ # redis.xinfo(:stream, 'mystream')
2936
+ # @example groups
2937
+ # redis.xinfo(:groups, 'mystream')
2938
+ # @example consumers
2939
+ # redis.xinfo(:consumers, 'mystream', 'mygroup')
2940
+ #
2941
+ # @param subcommand [String] e.g. `stream` `groups` `consumers`
2942
+ # @param key [String] the stream key
2943
+ # @param group [String] the consumer group name, required if subcommand is `consumers`
2944
+ #
2945
+ # @return [Hash] information of the stream if subcommand is `stream`
2946
+ # @return [Array<Hash>] information of the consumer groups if subcommand is `groups`
2947
+ # @return [Array<Hash>] information of the consumers if subcommand is `consumers`
2948
+ def xinfo(subcommand, key, group = nil)
2949
+ args = [:xinfo, subcommand, key, group].compact
2950
+ synchronize do |client|
2951
+ client.call(args) do |reply|
2952
+ case subcommand.to_s.downcase
2953
+ when 'stream' then Hashify.call(reply)
2954
+ when 'groups', 'consumers' then reply.map { |arr| Hashify.call(arr) }
2955
+ else reply
2956
+ end
2957
+ end
2958
+ end
2959
+ end
2960
+
2961
+ # Add new entry to the stream.
2962
+ #
2963
+ # @example Without options
2964
+ # redis.xadd('mystream', f1: 'v1', f2: 'v2')
2965
+ # @example With options
2966
+ # redis.xadd('mystream', { f1: 'v1', f2: 'v2' }, id: '0-0', maxlen: 1000, approximate: true)
2967
+ #
2968
+ # @param key [String] the stream key
2969
+ # @param entry [Hash] one or multiple field-value pairs
2970
+ # @param opts [Hash] several options for `XADD` command
2971
+ #
2972
+ # @option opts [String] :id the entry id, default value is `*`, it means auto generation
2973
+ # @option opts [Integer] :maxlen max length of entries
2974
+ # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen or not
2975
+ #
2976
+ # @return [String] the entry id
2977
+ def xadd(key, entry, approximate: nil, maxlen: nil, id: '*')
2978
+ args = [:xadd, key]
2979
+ if maxlen
2980
+ args << "MAXLEN"
2981
+ args << "~" if approximate
2982
+ args << maxlen
2983
+ end
2984
+ args << id
2985
+ args.concat(entry.to_a.flatten)
2986
+ synchronize { |client| client.call(args) }
2987
+ end
2988
+
2989
+ # Trims older entries of the stream if needed.
2990
+ #
2991
+ # @example Without options
2992
+ # redis.xtrim('mystream', 1000)
2993
+ # @example With options
2994
+ # redis.xtrim('mystream', 1000, approximate: true)
2995
+ #
2996
+ # @param key [String] the stream key
2997
+ # @param mexlen [Integer] max length of entries
2998
+ # @param approximate [Boolean] whether to add `~` modifier of maxlen or not
2999
+ #
3000
+ # @return [Integer] the number of entries actually deleted
3001
+ def xtrim(key, maxlen, approximate: false)
3002
+ args = [:xtrim, key, 'MAXLEN', (approximate ? '~' : nil), maxlen].compact
3003
+ synchronize { |client| client.call(args) }
3004
+ end
3005
+
3006
+ # Delete entries by entry ids.
3007
+ #
3008
+ # @example With splatted entry ids
3009
+ # redis.xdel('mystream', '0-1', '0-2')
3010
+ # @example With arrayed entry ids
3011
+ # redis.xdel('mystream', ['0-1', '0-2'])
3012
+ #
3013
+ # @param key [String] the stream key
3014
+ # @param ids [Array<String>] one or multiple entry ids
3015
+ #
3016
+ # @return [Integer] the number of entries actually deleted
3017
+ def xdel(key, *ids)
3018
+ args = [:xdel, key].concat(ids.flatten)
3019
+ synchronize { |client| client.call(args) }
3020
+ end
3021
+
3022
+ # Fetches entries of the stream in ascending order.
3023
+ #
3024
+ # @example Without options
3025
+ # redis.xrange('mystream')
3026
+ # @example With a specific start
3027
+ # redis.xrange('mystream', '0-1')
3028
+ # @example With a specific start and end
3029
+ # redis.xrange('mystream', '0-1', '0-3')
3030
+ # @example With count options
3031
+ # redis.xrange('mystream', count: 10)
3032
+ #
3033
+ # @param key [String] the stream key
3034
+ # @param start [String] first entry id of range, default value is `-`
3035
+ # @param end [String] last entry id of range, default value is `+`
3036
+ # @param count [Integer] the number of entries as limit
3037
+ #
3038
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3039
+ def xrange(key, start = '-', range_end = '+', count: nil)
3040
+ args = [:xrange, key, start, range_end]
3041
+ args.concat(['COUNT', count]) if count
3042
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3043
+ end
3044
+
3045
+ # Fetches entries of the stream in descending order.
3046
+ #
3047
+ # @example Without options
3048
+ # redis.xrevrange('mystream')
3049
+ # @example With a specific end
3050
+ # redis.xrevrange('mystream', '0-3')
3051
+ # @example With a specific end and start
3052
+ # redis.xrevrange('mystream', '0-3', '0-1')
3053
+ # @example With count options
3054
+ # redis.xrevrange('mystream', count: 10)
3055
+ #
3056
+ # @param key [String] the stream key
3057
+ # @param end [String] first entry id of range, default value is `+`
3058
+ # @param start [String] last entry id of range, default value is `-`
3059
+ # @params count [Integer] the number of entries as limit
3060
+ #
3061
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3062
+ def xrevrange(key, range_end = '+', start = '-', count: nil)
3063
+ args = [:xrevrange, key, range_end, start]
3064
+ args.concat(['COUNT', count]) if count
3065
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3066
+ end
3067
+
3068
+ # Returns the number of entries inside a stream.
3069
+ #
3070
+ # @example With key
3071
+ # redis.xlen('mystream')
3072
+ #
3073
+ # @param key [String] the stream key
3074
+ #
3075
+ # @return [Integer] the number of entries
3076
+ def xlen(key)
3077
+ synchronize { |client| client.call([:xlen, key]) }
3078
+ end
3079
+
3080
+ # Fetches entries from one or multiple streams. Optionally blocking.
3081
+ #
3082
+ # @example With a key
3083
+ # redis.xread('mystream', '0-0')
3084
+ # @example With multiple keys
3085
+ # redis.xread(%w[mystream1 mystream2], %w[0-0 0-0])
3086
+ # @example With count option
3087
+ # redis.xread('mystream', '0-0', count: 2)
3088
+ # @example With block option
3089
+ # redis.xread('mystream', '$', block: 1000)
3090
+ #
3091
+ # @param keys [Array<String>] one or multiple stream keys
3092
+ # @param ids [Array<String>] one or multiple entry ids
3093
+ # @param count [Integer] the number of entries as limit per stream
3094
+ # @param block [Integer] the number of milliseconds as blocking timeout
3095
+ #
3096
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3097
+ def xread(keys, ids, count: nil, block: nil)
3098
+ args = [:xread]
3099
+ args << 'COUNT' << count if count
3100
+ args << 'BLOCK' << block.to_i if block
3101
+ _xread(args, keys, ids, block)
3102
+ end
3103
+
3104
+ # Manages the consumer group of the stream.
3105
+ #
3106
+ # @example With `create` subcommand
3107
+ # redis.xgroup(:create, 'mystream', 'mygroup', '$')
3108
+ # @example With `setid` subcommand
3109
+ # redis.xgroup(:setid, 'mystream', 'mygroup', '$')
3110
+ # @example With `destroy` subcommand
3111
+ # redis.xgroup(:destroy, 'mystream', 'mygroup')
3112
+ # @example With `delconsumer` subcommand
3113
+ # redis.xgroup(:delconsumer, 'mystream', 'mygroup', 'consumer1')
3114
+ #
3115
+ # @param subcommand [String] `create` `setid` `destroy` `delconsumer`
3116
+ # @param key [String] the stream key
3117
+ # @param group [String] the consumer group name
3118
+ # @param id_or_consumer [String]
3119
+ # * the entry id or `$`, required if subcommand is `create` or `setid`
3120
+ # * the consumer name, required if subcommand is `delconsumer`
3121
+ # @param mkstream [Boolean] whether to create an empty stream automatically or not
3122
+ #
3123
+ # @return [String] `OK` if subcommand is `create` or `setid`
3124
+ # @return [Integer] effected count if subcommand is `destroy` or `delconsumer`
3125
+ def xgroup(subcommand, key, group, id_or_consumer = nil, mkstream: false)
3126
+ args = [:xgroup, subcommand, key, group, id_or_consumer, (mkstream ? 'MKSTREAM' : nil)].compact
3127
+ synchronize { |client| client.call(args) }
3128
+ end
3129
+
3130
+ # Fetches a subset of the entries from one or multiple streams related with the consumer group.
3131
+ # Optionally blocking.
3132
+ #
3133
+ # @example With a key
3134
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>')
3135
+ # @example With multiple keys
3136
+ # redis.xreadgroup('mygroup', 'consumer1', %w[mystream1 mystream2], %w[> >])
3137
+ # @example With count option
3138
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', count: 2)
3139
+ # @example With block option
3140
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', block: 1000)
3141
+ # @example With noack option
3142
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', noack: true)
3143
+ #
3144
+ # @param group [String] the consumer group name
3145
+ # @param consumer [String] the consumer name
3146
+ # @param keys [Array<String>] one or multiple stream keys
3147
+ # @param ids [Array<String>] one or multiple entry ids
3148
+ # @param opts [Hash] several options for `XREADGROUP` command
3149
+ #
3150
+ # @option opts [Integer] :count the number of entries as limit
3151
+ # @option opts [Integer] :block the number of milliseconds as blocking timeout
3152
+ # @option opts [Boolean] :noack whether message loss is acceptable or not
3153
+ #
3154
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3155
+ def xreadgroup(group, consumer, keys, ids, count: nil, block: nil, noack: nil)
3156
+ args = [:xreadgroup, 'GROUP', group, consumer]
3157
+ args << 'COUNT' << count if count
3158
+ args << 'BLOCK' << block.to_i if block
3159
+ args << 'NOACK' if noack
3160
+ _xread(args, keys, ids, block)
3161
+ end
3162
+
3163
+ # Removes one or multiple entries from the pending entries list of a stream consumer group.
3164
+ #
3165
+ # @example With a entry id
3166
+ # redis.xack('mystream', 'mygroup', '1526569495631-0')
3167
+ # @example With splatted entry ids
3168
+ # redis.xack('mystream', 'mygroup', '0-1', '0-2')
3169
+ # @example With arrayed entry ids
3170
+ # redis.xack('mystream', 'mygroup', %w[0-1 0-2])
3171
+ #
3172
+ # @param key [String] the stream key
3173
+ # @param group [String] the consumer group name
3174
+ # @param ids [Array<String>] one or multiple entry ids
3175
+ #
3176
+ # @return [Integer] the number of entries successfully acknowledged
3177
+ def xack(key, group, *ids)
3178
+ args = [:xack, key, group].concat(ids.flatten)
3179
+ synchronize { |client| client.call(args) }
3180
+ end
3181
+
3182
+ # Changes the ownership of a pending entry
3183
+ #
3184
+ # @example With splatted entry ids
3185
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-1', '0-2')
3186
+ # @example With arrayed entry ids
3187
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2])
3188
+ # @example With idle option
3189
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], idle: 1000)
3190
+ # @example With time option
3191
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], time: 1542866959000)
3192
+ # @example With retrycount option
3193
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], retrycount: 10)
3194
+ # @example With force option
3195
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], force: true)
3196
+ # @example With justid option
3197
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], justid: true)
3198
+ #
3199
+ # @param key [String] the stream key
3200
+ # @param group [String] the consumer group name
3201
+ # @param consumer [String] the consumer name
3202
+ # @param min_idle_time [Integer] the number of milliseconds
3203
+ # @param ids [Array<String>] one or multiple entry ids
3204
+ # @param opts [Hash] several options for `XCLAIM` command
3205
+ #
3206
+ # @option opts [Integer] :idle the number of milliseconds as last time it was delivered of the entry
3207
+ # @option opts [Integer] :time the number of milliseconds as a specific Unix Epoch time
3208
+ # @option opts [Integer] :retrycount the number of retry counter
3209
+ # @option opts [Boolean] :force whether to create the pending entry to the pending entries list or not
3210
+ # @option opts [Boolean] :justid whether to fetch just an array of entry ids or not
3211
+ #
3212
+ # @return [Hash{String => Hash}] the entries successfully claimed
3213
+ # @return [Array<String>] the entry ids successfully claimed if justid option is `true`
3214
+ def xclaim(key, group, consumer, min_idle_time, *ids, **opts)
3215
+ args = [:xclaim, key, group, consumer, min_idle_time].concat(ids.flatten)
3216
+ args.concat(['IDLE', opts[:idle].to_i]) if opts[:idle]
3217
+ args.concat(['TIME', opts[:time].to_i]) if opts[:time]
3218
+ args.concat(['RETRYCOUNT', opts[:retrycount]]) if opts[:retrycount]
3219
+ args << 'FORCE' if opts[:force]
3220
+ args << 'JUSTID' if opts[:justid]
3221
+ blk = opts[:justid] ? Noop : HashifyStreamEntries
3222
+ synchronize { |client| client.call(args, &blk) }
3223
+ end
3224
+
3225
+ # Fetches not acknowledging pending entries
3226
+ #
3227
+ # @example With key and group
3228
+ # redis.xpending('mystream', 'mygroup')
3229
+ # @example With range options
3230
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10)
3231
+ # @example With range and consumer options
3232
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10, 'consumer1')
3233
+ #
3234
+ # @param key [String] the stream key
3235
+ # @param group [String] the consumer group name
3236
+ # @param start [String] start first entry id of range
3237
+ # @param end [String] end last entry id of range
3238
+ # @param count [Integer] count the number of entries as limit
3239
+ # @param consumer [String] the consumer name
3240
+ #
3241
+ # @return [Hash] the summary of pending entries
3242
+ # @return [Array<Hash>] the pending entries details if options were specified
3243
+ def xpending(key, group, *args)
3244
+ command_args = [:xpending, key, group]
3245
+ case args.size
3246
+ when 0, 3, 4
3247
+ command_args.concat(args)
3248
+ else
3249
+ raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 2, 5 or 6)"
3250
+ end
3251
+
3252
+ summary_needed = args.empty?
3253
+ blk = summary_needed ? HashifyStreamPendings : HashifyStreamPendingDetails
3254
+ synchronize { |client| client.call(command_args, &blk) }
3255
+ end
3256
+
3257
+ # Interact with the sentinel command (masters, master, slaves, failover)
3258
+ #
3259
+ # @param [String] subcommand e.g. `masters`, `master`, `slaves`
3260
+ # @param [Array<String>] args depends on subcommand
3261
+ # @return [Array<String>, Hash<String, String>, String] depends on subcommand
3262
+ def sentinel(subcommand, *args)
3263
+ subcommand = subcommand.to_s.downcase
3264
+ synchronize do |client|
3265
+ client.call([:sentinel, subcommand] + args) do |reply|
3266
+ case subcommand
3267
+ when "get-master-addr-by-name"
3268
+ reply
3269
+ else
3270
+ if reply.is_a?(Array)
3271
+ if reply[0].is_a?(Array)
3272
+ reply.map(&Hashify)
3273
+ else
3274
+ Hashify.call(reply)
3275
+ end
3276
+ else
3277
+ reply
3278
+ end
3279
+ end
3280
+ end
3281
+ end
3282
+ end
3283
+
3284
+ # Sends `CLUSTER *` command to random node and returns its reply.
3285
+ #
3286
+ # @see https://redis.io/commands#cluster Reference of cluster command
3287
+ #
3288
+ # @param subcommand [String, Symbol] the subcommand of cluster command
3289
+ # e.g. `:slots`, `:nodes`, `:slaves`, `:info`
3290
+ #
3291
+ # @return [Object] depends on the subcommand
3292
+ def cluster(subcommand, *args)
3293
+ subcommand = subcommand.to_s.downcase
3294
+ block = case subcommand
3295
+ when 'slots'
3296
+ HashifyClusterSlots
3297
+ when 'nodes'
3298
+ HashifyClusterNodes
3299
+ when 'slaves'
3300
+ HashifyClusterSlaves
3301
+ when 'info'
3302
+ HashifyInfo
3303
+ else
3304
+ Noop
3305
+ end
3306
+
3307
+ # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
3308
+ block = Noop unless @cluster_mode
3309
+
3310
+ synchronize do |client|
3311
+ client.call([:cluster, subcommand] + args, &block)
3312
+ end
3313
+ end
3314
+
3315
+ # Sends `ASKING` command to random node and returns its reply.
3316
+ #
3317
+ # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
3318
+ #
3319
+ # @return [String] `'OK'`
3320
+ def asking
3321
+ synchronize { |client| client.call(%i[asking]) }
3322
+ end
3323
+
3324
+ def id
3325
+ @original_client.id
3326
+ end
3327
+
3328
+ def inspect
3329
+ "#<Redis client v#{Redis::VERSION} for #{id}>"
3330
+ end
3331
+
3332
+ def dup
3333
+ self.class.new(@options)
3334
+ end
3335
+
3336
+ def connection
3337
+ return @original_client.connection_info if @cluster_mode
3338
+
3339
+ {
3340
+ host: @original_client.host,
3341
+ port: @original_client.port,
3342
+ db: @original_client.db,
3343
+ id: @original_client.id,
3344
+ location: @original_client.location
3345
+ }
3346
+ end
3347
+
3348
+ def method_missing(command, *args) # rubocop:disable Style/MissingRespondToMissing
3349
+ synchronize do |client|
3350
+ client.call([command] + args)
3351
+ end
3352
+ end
3353
+
3354
+ private
2195
3355
 
2196
3356
  # Commands returning 1 for true and 0 for false may be executed in a pipeline
2197
3357
  # where the method call will return nil. Propagate the nil instead of falsely
2198
3358
  # returning false.
2199
- def _boolify
2200
- lambda { |value|
2201
- value == 1 if value
2202
- }
2203
- end
3359
+ Boolify = lambda { |value|
3360
+ case value
3361
+ when 1
3362
+ true
3363
+ when 0
3364
+ false
3365
+ else
3366
+ value
3367
+ end
3368
+ }
2204
3369
 
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
3370
+ BoolifySet = lambda { |value|
3371
+ case value
3372
+ when "OK"
3373
+ true
3374
+ when nil
3375
+ false
3376
+ else
3377
+ value
3378
+ end
3379
+ }
3380
+
3381
+ Hashify = lambda { |value|
3382
+ if value.respond_to?(:each_slice)
3383
+ value.each_slice(2).to_h
3384
+ else
3385
+ value
3386
+ end
3387
+ }
3388
+
3389
+ Floatify = lambda { |value|
3390
+ case value
3391
+ when "inf"
3392
+ Float::INFINITY
3393
+ when "-inf"
3394
+ -Float::INFINITY
3395
+ when String
3396
+ Float(value)
3397
+ else
3398
+ value
3399
+ end
3400
+ }
3401
+
3402
+ FloatifyPairs = lambda { |value|
3403
+ return value unless value.respond_to?(:each_slice)
3404
+
3405
+ value.each_slice(2).map do |member, score|
3406
+ [member, Floatify.call(score)]
3407
+ end
3408
+ }
3409
+
3410
+ HashifyInfo = lambda { |reply|
3411
+ lines = reply.split("\r\n").grep_v(/^(#|$)/)
3412
+ lines.map! { |line| line.split(':', 2) }
3413
+ lines.compact!
3414
+ lines.to_h
3415
+ }
3416
+
3417
+ HashifyStreams = lambda { |reply|
3418
+ case reply
3419
+ when nil
3420
+ {}
3421
+ else
3422
+ reply.map { |key, entries| [key, HashifyStreamEntries.call(entries)] }.to_h
3423
+ end
3424
+ }
3425
+
3426
+ EMPTY_STREAM_RESPONSE = [nil].freeze
3427
+ private_constant :EMPTY_STREAM_RESPONSE
3428
+
3429
+ HashifyStreamEntries = lambda { |reply|
3430
+ reply.compact.map do |entry_id, values|
3431
+ [entry_id, values.each_slice(2).to_h]
3432
+ end
3433
+ }
3434
+
3435
+ HashifyStreamPendings = lambda { |reply|
3436
+ {
3437
+ 'size' => reply[0],
3438
+ 'min_entry_id' => reply[1],
3439
+ 'max_entry_id' => reply[2],
3440
+ 'consumers' => reply[3].nil? ? {} : reply[3].to_h
2212
3441
  }
3442
+ }
3443
+
3444
+ HashifyStreamPendingDetails = lambda { |reply|
3445
+ reply.map do |arr|
3446
+ {
3447
+ 'entry_id' => arr[0],
3448
+ 'consumer' => arr[1],
3449
+ 'elapsed' => arr[2],
3450
+ 'count' => arr[3]
3451
+ }
3452
+ end
3453
+ }
3454
+
3455
+ HashifyClusterNodeInfo = lambda { |str|
3456
+ arr = str.split(' ')
3457
+ {
3458
+ 'node_id' => arr[0],
3459
+ 'ip_port' => arr[1],
3460
+ 'flags' => arr[2].split(','),
3461
+ 'master_node_id' => arr[3],
3462
+ 'ping_sent' => arr[4],
3463
+ 'pong_recv' => arr[5],
3464
+ 'config_epoch' => arr[6],
3465
+ 'link_state' => arr[7],
3466
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
3467
+ }
3468
+ }
3469
+
3470
+ HashifyClusterSlots = lambda { |reply|
3471
+ reply.map do |arr|
3472
+ first_slot, last_slot = arr[0..1]
3473
+ master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
3474
+ replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
3475
+ {
3476
+ 'start_slot' => first_slot,
3477
+ 'end_slot' => last_slot,
3478
+ 'master' => master,
3479
+ 'replicas' => replicas
3480
+ }
3481
+ end
3482
+ }
3483
+
3484
+ HashifyClusterNodes = lambda { |reply|
3485
+ reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
3486
+ }
3487
+
3488
+ HashifyClusterSlaves = lambda { |reply|
3489
+ reply.map { |str| HashifyClusterNodeInfo.call(str) }
3490
+ }
3491
+
3492
+ Noop = ->(reply) { reply }
3493
+
3494
+ def _geoarguments(*args, options: nil, sort: nil, count: nil)
3495
+ args.push sort if sort
3496
+ args.push 'count', count if count
3497
+ args.push options if options
3498
+ args
2213
3499
  end
2214
3500
 
2215
- def _subscription(method, channels, block)
2216
- return @client.call [method, *channels] if subscribed?
3501
+ def _subscription(method, timeout, channels, block)
3502
+ return @client.call([method] + channels) if subscribed?
2217
3503
 
2218
3504
  begin
2219
3505
  original, @client = @client, SubscribedClient.new(@client)
2220
- @client.send(method, *channels, &block)
3506
+ if timeout > 0
3507
+ @client.send(method, timeout, *channels, &block)
3508
+ else
3509
+ @client.send(method, *channels, &block)
3510
+ end
2221
3511
  ensure
2222
3512
  @client = original
2223
3513
  end
2224
3514
  end
2225
3515
 
3516
+ def _xread(args, keys, ids, blocking_timeout_msec)
3517
+ keys = keys.is_a?(Array) ? keys : [keys]
3518
+ ids = ids.is_a?(Array) ? ids : [ids]
3519
+ args << 'STREAMS'
3520
+ args.concat(keys)
3521
+ args.concat(ids)
3522
+
3523
+ synchronize do |client|
3524
+ if blocking_timeout_msec.nil?
3525
+ client.call(args, &HashifyStreams)
3526
+ elsif blocking_timeout_msec.to_f.zero?
3527
+ client.call_without_timeout(args, &HashifyStreams)
3528
+ else
3529
+ timeout = client.timeout.to_f + blocking_timeout_msec.to_f / 1000.0
3530
+ client.call_with_timeout(args, timeout, &HashifyStreams)
3531
+ end
3532
+ end
3533
+ end
2226
3534
  end
2227
3535
 
2228
- require "redis/version"
2229
- require "redis/connection"
2230
- require "redis/client"
2231
- require "redis/pipeline"
2232
- require "redis/subscribe"
3536
+ require_relative "redis/version"
3537
+ require_relative "redis/connection"
3538
+ require_relative "redis/client"
3539
+ require_relative "redis/cluster"
3540
+ require_relative "redis/pipeline"
3541
+ require_relative "redis/subscribe"