redis 3.0.0 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +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"