redis 3.3.5 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (126) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -2
  3. data/README.md +77 -76
  4. data/lib/redis.rb +779 -63
  5. data/lib/redis/client.rb +41 -20
  6. data/lib/redis/cluster.rb +286 -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 +104 -0
  11. data/lib/redis/cluster/node_key.rb +35 -0
  12. data/lib/redis/cluster/node_loader.rb +37 -0
  13. data/lib/redis/cluster/option.rb +77 -0
  14. data/lib/redis/cluster/slot.rb +69 -0
  15. data/lib/redis/cluster/slot_loader.rb +49 -0
  16. data/lib/redis/connection.rb +2 -2
  17. data/lib/redis/connection/command_helper.rb +2 -8
  18. data/lib/redis/connection/hiredis.rb +2 -2
  19. data/lib/redis/connection/ruby.rb +13 -30
  20. data/lib/redis/connection/synchrony.rb +12 -4
  21. data/lib/redis/distributed.rb +32 -12
  22. data/lib/redis/errors.rb +46 -0
  23. data/lib/redis/hash_ring.rb +20 -64
  24. data/lib/redis/pipeline.rb +9 -7
  25. data/lib/redis/version.rb +1 -1
  26. metadata +53 -196
  27. data/.gitignore +0 -16
  28. data/.travis.yml +0 -89
  29. data/.travis/Gemfile +0 -11
  30. data/.yardopts +0 -3
  31. data/Gemfile +0 -4
  32. data/Rakefile +0 -87
  33. data/benchmarking/logging.rb +0 -71
  34. data/benchmarking/pipeline.rb +0 -51
  35. data/benchmarking/speed.rb +0 -21
  36. data/benchmarking/suite.rb +0 -24
  37. data/benchmarking/worker.rb +0 -71
  38. data/examples/basic.rb +0 -15
  39. data/examples/consistency.rb +0 -114
  40. data/examples/dist_redis.rb +0 -43
  41. data/examples/incr-decr.rb +0 -17
  42. data/examples/list.rb +0 -26
  43. data/examples/pubsub.rb +0 -37
  44. data/examples/sentinel.rb +0 -41
  45. data/examples/sentinel/start +0 -49
  46. data/examples/sets.rb +0 -36
  47. data/examples/unicorn/config.ru +0 -3
  48. data/examples/unicorn/unicorn.rb +0 -20
  49. data/redis.gemspec +0 -44
  50. data/test/bitpos_test.rb +0 -69
  51. data/test/blocking_commands_test.rb +0 -42
  52. data/test/client_test.rb +0 -59
  53. data/test/command_map_test.rb +0 -30
  54. data/test/commands_on_hashes_test.rb +0 -21
  55. data/test/commands_on_hyper_log_log_test.rb +0 -21
  56. data/test/commands_on_lists_test.rb +0 -20
  57. data/test/commands_on_sets_test.rb +0 -77
  58. data/test/commands_on_sorted_sets_test.rb +0 -137
  59. data/test/commands_on_strings_test.rb +0 -101
  60. data/test/commands_on_value_types_test.rb +0 -133
  61. data/test/connection_handling_test.rb +0 -277
  62. data/test/connection_test.rb +0 -57
  63. data/test/distributed_blocking_commands_test.rb +0 -46
  64. data/test/distributed_commands_on_hashes_test.rb +0 -10
  65. data/test/distributed_commands_on_hyper_log_log_test.rb +0 -33
  66. data/test/distributed_commands_on_lists_test.rb +0 -22
  67. data/test/distributed_commands_on_sets_test.rb +0 -83
  68. data/test/distributed_commands_on_sorted_sets_test.rb +0 -18
  69. data/test/distributed_commands_on_strings_test.rb +0 -59
  70. data/test/distributed_commands_on_value_types_test.rb +0 -95
  71. data/test/distributed_commands_requiring_clustering_test.rb +0 -164
  72. data/test/distributed_connection_handling_test.rb +0 -23
  73. data/test/distributed_internals_test.rb +0 -79
  74. data/test/distributed_key_tags_test.rb +0 -52
  75. data/test/distributed_persistence_control_commands_test.rb +0 -26
  76. data/test/distributed_publish_subscribe_test.rb +0 -92
  77. data/test/distributed_remote_server_control_commands_test.rb +0 -66
  78. data/test/distributed_scripting_test.rb +0 -102
  79. data/test/distributed_sorting_test.rb +0 -20
  80. data/test/distributed_test.rb +0 -58
  81. data/test/distributed_transactions_test.rb +0 -32
  82. data/test/encoding_test.rb +0 -18
  83. data/test/error_replies_test.rb +0 -59
  84. data/test/fork_safety_test.rb +0 -65
  85. data/test/helper.rb +0 -232
  86. data/test/helper_test.rb +0 -24
  87. data/test/internals_test.rb +0 -417
  88. data/test/lint/blocking_commands.rb +0 -150
  89. data/test/lint/hashes.rb +0 -162
  90. data/test/lint/hyper_log_log.rb +0 -60
  91. data/test/lint/lists.rb +0 -143
  92. data/test/lint/sets.rb +0 -140
  93. data/test/lint/sorted_sets.rb +0 -316
  94. data/test/lint/strings.rb +0 -260
  95. data/test/lint/value_types.rb +0 -122
  96. data/test/persistence_control_commands_test.rb +0 -26
  97. data/test/pipelining_commands_test.rb +0 -242
  98. data/test/publish_subscribe_test.rb +0 -282
  99. data/test/remote_server_control_commands_test.rb +0 -118
  100. data/test/scanning_test.rb +0 -413
  101. data/test/scripting_test.rb +0 -78
  102. data/test/sentinel_command_test.rb +0 -80
  103. data/test/sentinel_test.rb +0 -255
  104. data/test/sorting_test.rb +0 -59
  105. data/test/ssl_test.rb +0 -73
  106. data/test/support/connection/hiredis.rb +0 -1
  107. data/test/support/connection/ruby.rb +0 -1
  108. data/test/support/connection/synchrony.rb +0 -17
  109. data/test/support/redis_mock.rb +0 -130
  110. data/test/support/ssl/gen_certs.sh +0 -31
  111. data/test/support/ssl/trusted-ca.crt +0 -25
  112. data/test/support/ssl/trusted-ca.key +0 -27
  113. data/test/support/ssl/trusted-cert.crt +0 -81
  114. data/test/support/ssl/trusted-cert.key +0 -28
  115. data/test/support/ssl/untrusted-ca.crt +0 -26
  116. data/test/support/ssl/untrusted-ca.key +0 -27
  117. data/test/support/ssl/untrusted-cert.crt +0 -82
  118. data/test/support/ssl/untrusted-cert.key +0 -28
  119. data/test/support/wire/synchrony.rb +0 -24
  120. data/test/support/wire/thread.rb +0 -5
  121. data/test/synchrony_driver.rb +0 -88
  122. data/test/test.conf.erb +0 -9
  123. data/test/thread_safety_test.rb +0 -62
  124. data/test/transactions_test.rb +0 -264
  125. data/test/unknown_commands_test.rb +0 -14
  126. data/test/url_param_test.rb +0 -138
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 729f56810aa501065e2fc203050363713c80ee37
4
- data.tar.gz: e7e13fb75618f794a2c742d73d6c8a72699d5e92
3
+ metadata.gz: fb5e8fcd26f9729131009cc0c25bed4cdf1009f5
4
+ data.tar.gz: 9ed7be2cc83d01dcb6c69002680105dc544e659a
5
5
  SHA512:
6
- metadata.gz: f87e76c751760b7f1feb7c859ea373c41ebf805c104c27e85177cb20a60d42b11c50db2d5a916fb7bfc78b82b13141770a851568aea922ccc1970f4e30ba1dea
7
- data.tar.gz: ac4cb236c9c9897d73ead9421e161cf1c7aac2a3c1f77c088b5c52c324cddccda6ab749441093cbcc94ea812e70232f9f9ac1c160a96fd1fbd46d87586d1d7fb
6
+ metadata.gz: b39badbb4689a4ea93cbc65ad00f0c967f24dadcc41e7750ab82977176236d6d8609d8e7b74e13a3829ce008c4a5de90930e265f6b6e03af710811419da6ccf6
7
+ data.tar.gz: 3ede92146cb181328657a4713e94473f86661dffe0a66d23f48edaa2c7a6c52543a7e7036794baa9dcc2945bc3f9e5eebd9192b70510bda1ad9c0d6e67253830
@@ -1,8 +1,60 @@
1
+ # Unreleased
2
+
3
+ # 4.1.0
4
+
5
+ * Add Redis Cluster support. See #716.
6
+ * Add streams support. See #799 and #811.
7
+ * Add ZPOP* support. See #812.
8
+ * Fix issues with integer-like objects as BPOP timeout
9
+
10
+ # 4.0.3
11
+
12
+ * Fix raising command error for first command in pipeline. See #788.
13
+ * Fix the gemspec to stop exposing a `build` executable. See #785.
14
+ * Add `:reconnect_delay` and `:reconnect_delay_max` options. See #778.
15
+
16
+ # 4.0.2
17
+
18
+ * Added `Redis#unlink`. See #766.
19
+
20
+ * `Redis.new` now accept a custom connector via `:connector`. See #591.
21
+
22
+ * `Redis#multi` no longer perform empty transactions. See #747.
23
+
24
+ * `Redis#hdel` now accepts hash keys as multiple arguments like `#del`. See #755.
25
+
26
+ * Allow to skip SSL verification. See #745.
27
+
28
+ * Add Geo commands: `geoadd`, `geohash`, `georadius`, `georadiusbymember`, `geopos`, `geodist`. See #730.
29
+
30
+ # 4.0.1
31
+
32
+ * `Redis::Distributed` now supports `mget` and `mapped_mget`. See #687.
33
+
34
+ * `Redis::Distributed` now supports `sscan` and `sscan_each`. See #572.
35
+
36
+ * `Redis#connection` returns a hash with connection information.
37
+ You shouldn't need to call `Redis#_client`, ever.
38
+
39
+ * `Redis#flushdb` and `Redis#flushall` now support the `:async` option. See #706.
40
+
41
+
42
+ # 4.0
43
+
44
+ * Removed `Redis.connect`. Use `Redis.new`.
45
+
46
+ * Removed `Redis#[]` and `Redis#[]=` aliases.
47
+
48
+ * Added support for `CLIENT` commands. The lower-level client can be
49
+ accessed via `Redis#_client`.
50
+
51
+ * Dropped official support for Ruby < 2.2.2.
52
+
1
53
  # 3.3.5
2
54
 
3
55
  * Fixed Ruby 1.8 compatibility after backporting `Redis#connection`. See #719.
4
56
 
5
- # 3.3.4
57
+ # 3.3.4 (yanked)
6
58
 
7
59
  * `Redis#connection` returns a hash with connection information.
8
60
  You shouldn't need to call `Redis#_client`, ever.
@@ -13,7 +65,7 @@
13
65
 
14
66
  # 3.3.2
15
67
 
16
- * Added support for SPOP with COUNT. See #628.
68
+ * Added support for `SPOP` with COUNT. See #628.
17
69
 
18
70
  * Fixed connection glitches when using SSL. See #644.
19
71
 
data/README.md CHANGED
@@ -1,46 +1,17 @@
1
1
  # redis-rb [![Build Status][travis-image]][travis-link] [![Inline docs][inchpages-image]][inchpages-link]
2
2
 
3
- [travis-image]: https://secure.travis-ci.org/redis/redis-rb.png?branch=master
4
- [travis-link]: http://travis-ci.org/redis/redis-rb
5
- [travis-home]: http://travis-ci.org/
6
- [inchpages-image]: http://inch-ci.org/github/redis/redis-rb.png
7
- [inchpages-link]: http://inch-ci.org/github/redis/redis-rb
3
+ A Ruby client that tries to match [Redis][redis-home]' API one-to-one, while still
4
+ providing an idiomatic interface.
8
5
 
9
- A Ruby client library for [Redis][redis-home].
10
-
11
- [redis-home]: http://redis.io
12
-
13
- A Ruby client that tries to match Redis' API one-to-one, while still
14
- providing an idiomatic interface. It features thread-safety, client-side
15
- sharding, pipelining, and an obsession for performance.
16
-
17
- ## Upgrading from 2.x to 3.0
18
-
19
- Please refer to the [CHANGELOG][changelog-3.0.0] for a summary of the
20
- most important changes, as well as a full list of changes.
21
-
22
- [changelog-3.0.0]: https://github.com/redis/redis-rb/blob/master/CHANGELOG.md#300
23
6
 
24
7
  ## Getting started
25
8
 
26
- To install **redis-rb**, run the following command:
9
+ Install with:
27
10
 
28
11
  ```
29
- gem install redis
12
+ $ gem install redis
30
13
  ```
31
14
 
32
- Or if you are using **bundler**, add
33
-
34
- ```
35
- gem 'redis', '~>3.2'
36
- ```
37
-
38
- to your `Gemfile`, and run `bundle install`
39
-
40
- As of version 2.0 this client only targets Redis version 2.0 and higher.
41
- You can use an older version of this client if you need to interface
42
- with a Redis instance older than 2.0, but this is no longer supported.
43
-
44
15
  You can connect to Redis by instantiating the `Redis` class:
45
16
 
46
17
  ```ruby
@@ -54,17 +25,15 @@ listening on `localhost`, port 6379. If you need to connect to a remote
54
25
  server or a different port, try:
55
26
 
56
27
  ```ruby
57
- redis = Redis.new(:host => "10.0.1.1", :port => 6380, :db => 15)
28
+ redis = Redis.new(host: "10.0.1.1", port: 6380, db: 15)
58
29
  ```
59
30
 
60
31
  You can also specify connection options as a [`redis://` URL][redis-url]:
61
32
 
62
33
  ```ruby
63
- redis = Redis.new(:url => "redis://:p4ssw0rd@10.0.1.1:6380/15")
34
+ redis = Redis.new(url: "redis://:p4ssw0rd@10.0.1.1:6380/15")
64
35
  ```
65
36
 
66
- [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
67
-
68
37
  By default, the client will try to read the `REDIS_URL` environment variable
69
38
  and use that as URL to connect to. The above statement is therefore equivalent
70
39
  to setting this environment variable and calling `Redis.new` without arguments.
@@ -72,13 +41,13 @@ to setting this environment variable and calling `Redis.new` without arguments.
72
41
  To connect to Redis listening on a Unix socket, try:
73
42
 
74
43
  ```ruby
75
- redis = Redis.new(:path => "/tmp/redis.sock")
44
+ redis = Redis.new(path: "/tmp/redis.sock")
76
45
  ```
77
46
 
78
47
  To connect to a password protected Redis instance, use:
79
48
 
80
49
  ```ruby
81
- redis = Redis.new(:password => "mysecret")
50
+ redis = Redis.new(password: "mysecret")
82
51
  ```
83
52
 
84
53
  The Redis class exports methods that are named identical to the commands
@@ -86,8 +55,6 @@ they execute. The arguments these methods accept are often identical to
86
55
  the arguments specified on the [Redis website][redis-commands]. For
87
56
  instance, the `SET` and `GET` commands can be called like this:
88
57
 
89
- [redis-commands]: http://redis.io/commands
90
-
91
58
  ```ruby
92
59
  redis.set("mykey", "hello world")
93
60
  # => "OK"
@@ -96,24 +63,22 @@ redis.get("mykey")
96
63
  # => "hello world"
97
64
  ```
98
65
 
99
- All commands, their arguments and return values are documented, and
100
- available on [rdoc.info][rdoc].
101
-
102
- [rdoc]: http://rdoc.info/github/redis/redis-rb/
66
+ All commands, their arguments, and return values are documented and
67
+ available on [RubyDoc.info][rubydoc].
103
68
 
104
69
  ## Sentinel support
105
70
 
106
- The client is able to perform automatic failovers by using [Redis
71
+ The client is able to perform automatic failover by using [Redis
107
72
  Sentinel](http://redis.io/topics/sentinel). Make sure to run Redis 2.8+
108
73
  if you want to use this feature.
109
74
 
110
75
  To connect using Sentinel, use:
111
76
 
112
77
  ```ruby
113
- SENTINELS = [{:host => "127.0.0.1", :port => 26380},
114
- {:host => "127.0.0.1", :port => 26381}]
78
+ SENTINELS = [{ host: "127.0.0.1", port: 26380 },
79
+ { host: "127.0.0.1", port: 26381 }]
115
80
 
116
- redis = Redis.new(:url => "redis://mymaster", :sentinels => SENTINELS, :role => :master)
81
+ redis = Redis.new(url: "redis://mymaster", sentinels: SENTINELS, role: :master)
117
82
  ```
118
83
 
119
84
  * The master name identifies a group of Redis instances composed of a master
@@ -211,7 +176,7 @@ it can't connect to the server a `Redis::CannotConnectError` error will be raise
211
176
  ```ruby
212
177
  begin
213
178
  redis.ping
214
- rescue Exception => e
179
+ rescue StandardError => e
215
180
  e.inspect
216
181
  # => #<Redis::CannotConnectError: Timed out connecting to Redis on 10.0.1.1:6380>
217
182
 
@@ -255,6 +220,51 @@ end
255
220
 
256
221
  If no message is received after 5 seconds, the client will unsubscribe.
257
222
 
223
+ ## Reconnections
224
+
225
+ The client allows you to configure how many `reconnect_attempts` it should
226
+ complete before declaring a connection as failed. Furthermore, you may want
227
+ to control the maximum duration between reconnection attempts with
228
+ `reconnect_delay` and `reconnect_delay_max`.
229
+
230
+ ```ruby
231
+ Redis.new(
232
+ :reconnect_attempts => 10,
233
+ :reconnect_delay => 1.5,
234
+ :reconnect_delay_max => 10.0,
235
+ )
236
+ ```
237
+
238
+ The delay values are specified in seconds. With the above configuration, the
239
+ client would attempt 10 reconnections, exponentially increasing the duration
240
+ between each attempt but it never waits longer than `reconnect_delay_max`.
241
+
242
+ This is the retry algorithm:
243
+
244
+ ```ruby
245
+ attempt_wait_time = [(reconnect_delay * 2**(attempt-1)), reconnect_delay_max].min
246
+ ```
247
+
248
+ **By default**, this gem will only **retry a connection once** and then fail, but with the
249
+ above configuration the reconnection attempt would look like this:
250
+
251
+ #|Attempt wait time|Total wait time
252
+ :-:|:-:|:-:
253
+ 1|1.5s|1.5s
254
+ 2|3.0s|4.5s
255
+ 3|6.0s|10.5s
256
+ 4|10.0s|20.5s
257
+ 5|10.0s|30.5s
258
+ 6|10.0s|40.5s
259
+ 7|10.0s|50.5s
260
+ 8|10.0s|60.5s
261
+ 9|10.0s|70.5s
262
+ 10|10.0s|80.5s
263
+
264
+ So if the reconnection attempt #10 succeeds 70 seconds have elapsed trying
265
+ to reconnect, this is likely fine in long-running background processes, but if
266
+ you use Redis to drive your website you might want to have a lower
267
+ `reconnect_delay_max` or have less `reconnect_attempts`.
258
268
 
259
269
  ## SSL/TLS Support
260
270
 
@@ -374,37 +384,28 @@ redis = Redis.new(:driver => :synchrony)
374
384
 
375
385
  ## Testing
376
386
 
377
- This library is tested using [Travis][travis-home], where it is tested
378
- against the following interpreters and drivers:
379
-
380
- * MRI 1.8.7 (drivers: ruby, hiredis)
381
- * MRI 1.9.3 (drivers: ruby, hiredis, synchrony)
382
- * MRI 2.0 (drivers: ruby, hiredis, synchrony)
383
- * MRI 2.1 (drivers: ruby, hiredis, synchrony)
384
- * MRI 2.2 (drivers: ruby, hiredis, synchrony)
385
- * MRI 2.3 (drivers: ruby, hiredis, synchrony)
386
- * JRuby 1.7 (1.8 mode) (drivers: ruby)
387
- * JRuby 1.7 (1.9 mode) (drivers: ruby)
387
+ This library is tested against recent Ruby and Redis versions.
388
+ Check [Travis][travis-link] for the exact versions supported.
388
389
 
389
390
  ## Contributors
390
391
 
391
- (ordered chronologically with more than 5 commits, see `git shortlog -sn` for
392
- all contributors)
393
-
394
- * Ezra Zygmuntowicz
395
- * Taylor Weibley
396
- * Matthew Clark
397
- * Brian McKinney
398
- * Luca Guidi
399
- * Salvatore Sanfilippo
400
- * Chris Wanstrath
401
- * Damian Janowski
402
- * Michel Martens
403
- * Nick Quaranto
404
- * Pieter Noordhuis
405
- * Ilya Grigorik
392
+ Several people contributed to redis-rb, but we would like to especially
393
+ mention Ezra Zygmuntowicz. Ezra introduced the Ruby community to many
394
+ new cool technologies, like Redis. He wrote the first version of this
395
+ client and evangelized Redis in Rubyland. Thank you, Ezra.
406
396
 
407
397
  ## Contributing
408
398
 
409
399
  [Fork the project](https://github.com/redis/redis-rb) and send pull
410
400
  requests. You can also ask for help at `#redis-rb` on Freenode.
401
+
402
+
403
+ [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
404
+ [inchpages-link]: https://inch-ci.org/github/redis/redis-rb
405
+ [redis-commands]: https://redis.io/commands
406
+ [redis-home]: https://redis.io
407
+ [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
408
+ [travis-home]: https://travis-ci.org/
409
+ [travis-image]: https://secure.travis-ci.org/redis/redis-rb.svg?branch=master
410
+ [travis-link]: https://travis-ci.org/redis/redis-rb
411
+ [rubydoc]: http://www.rubydoc.info/gems/redis
@@ -1,21 +1,8 @@
1
1
  require "monitor"
2
- require "redis/errors"
2
+ require_relative "redis/errors"
3
3
 
4
4
  class Redis
5
5
 
6
- def self.deprecate(message, trace = caller[0])
7
- $stderr.puts "\n#{message} (in #{trace})"
8
- end
9
-
10
- attr :client
11
-
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)
17
- end
18
-
19
6
  def self.current
20
7
  @current ||= Redis.new
21
8
  end
@@ -44,11 +31,16 @@ class Redis
44
31
  # @option options [Boolean] :inherit_socket (false) Whether to use socket in forked process or not
45
32
  # @option options [Array] :sentinels List of sentinels to contact
46
33
  # @option options [Symbol] :role (:master) Role to fetch via Sentinel, either `:master` or `:slave`
34
+ # @option options [Array<String, Hash{Symbol => String, Integer}>] :cluster List of cluster nodes to contact
35
+ # @option options [Boolean] :replica Whether to use readonly replica nodes in Redis Cluster or not
36
+ # @option options [Class] :connector Class of custom connector
47
37
  #
48
38
  # @return [Redis] a new client instance
49
39
  def initialize(options = {})
50
40
  @options = options.dup
51
- @original_client = @client = Client.new(options)
41
+ @cluster_mode = options.key?(:cluster)
42
+ client = @cluster_mode ? Cluster : Client
43
+ @original_client = @client = client.new(options)
52
44
  @queue = Hash.new { |h, k| h[k] = [] }
53
45
 
54
46
  super() # Monitor#initialize
@@ -119,6 +111,10 @@ class Redis
119
111
  end
120
112
  end
121
113
 
114
+ def _client
115
+ @client
116
+ end
117
+
122
118
  # Authenticate to the server.
123
119
  #
124
120
  # @param [String] password must match the password specified in the
@@ -143,10 +139,11 @@ class Redis
143
139
 
144
140
  # Ping the server.
145
141
  #
142
+ # @param [optional, String] message
146
143
  # @return [String] `PONG`
147
- def ping
144
+ def ping(message = nil)
148
145
  synchronize do |client|
149
- client.call([:ping])
146
+ client.call([:ping, message].compact)
150
147
  end
151
148
  end
152
149
 
@@ -209,6 +206,25 @@ class Redis
209
206
  end
210
207
  end
211
208
 
209
+ # Manage client connections.
210
+ #
211
+ # @param [String, Symbol] subcommand e.g. `kill`, `list`, `getname`, `setname`
212
+ # @return [String, Hash] depends on subcommand
213
+ def client(subcommand = nil, *args)
214
+ synchronize do |client|
215
+ client.call([:client, subcommand] + args) do |reply|
216
+ if subcommand.to_s == "list"
217
+ reply.lines.map do |line|
218
+ entries = line.chomp.split(/[ =]/)
219
+ Hash[entries.each_slice(2).to_a]
220
+ end
221
+ else
222
+ reply
223
+ end
224
+ end
225
+ end
226
+ end
227
+
212
228
  # Return the number of keys in the selected database.
213
229
  #
214
230
  # @return [Fixnum]
@@ -226,19 +242,31 @@ class Redis
226
242
 
227
243
  # Remove all keys from all databases.
228
244
  #
245
+ # @param [Hash] options
246
+ # - `:async => Boolean`: async flush (default: false)
229
247
  # @return [String] `OK`
230
- def flushall
248
+ def flushall(options = nil)
231
249
  synchronize do |client|
232
- client.call([:flushall])
250
+ if options && options[:async]
251
+ client.call([:flushall, :async])
252
+ else
253
+ client.call([:flushall])
254
+ end
233
255
  end
234
256
  end
235
257
 
236
258
  # Remove all keys from the current database.
237
259
  #
260
+ # @param [Hash] options
261
+ # - `:async => Boolean`: async flush (default: false)
238
262
  # @return [String] `OK`
239
- def flushdb
263
+ def flushdb(options = nil)
240
264
  synchronize do |client|
241
- client.call([:flushdb])
265
+ if options && options[:async]
266
+ client.call([:flushdb, :async])
267
+ else
268
+ client.call([:flushdb])
269
+ end
242
270
  end
243
271
  end
244
272
 
@@ -250,9 +278,7 @@ class Redis
250
278
  synchronize do |client|
251
279
  client.call([:info, cmd].compact) do |reply|
252
280
  if reply.kind_of?(String)
253
- reply = Hash[reply.split("\r\n").map do |line|
254
- line.split(":", 2) unless line =~ /^(#|$)/
255
- end.compact]
281
+ reply = HashifyInfo.call(reply)
256
282
 
257
283
  if cmd && cmd.to_s == "commandstats"
258
284
  # Extract nested hashes for INFO COMMANDSTATS
@@ -458,31 +484,42 @@ class Redis
458
484
  # @param [String] key
459
485
  # @param [String] ttl
460
486
  # @param [String] serialized_value
487
+ # @param [Hash] options
488
+ # - `:replace => Boolean`: if false, raises an error if key already exists
489
+ # @raise [Redis::CommandError]
461
490
  # @return [String] `"OK"`
462
- def restore(key, ttl, serialized_value)
491
+ def restore(key, ttl, serialized_value, options = {})
492
+ args = [:restore, key, ttl, serialized_value]
493
+ args << 'REPLACE' if options[:replace]
494
+
463
495
  synchronize do |client|
464
- client.call([:restore, key, ttl, serialized_value])
496
+ client.call(args)
465
497
  end
466
498
  end
467
499
 
468
500
  # Transfer a key from the connected instance to another instance.
469
501
  #
470
- # @param [String] key
502
+ # @param [String, Array<String>] key
471
503
  # @param [Hash] options
472
504
  # - `:host => String`: host of instance to migrate to
473
505
  # - `:port => Integer`: port of instance to migrate to
474
506
  # - `:db => Integer`: database to migrate to (default: same as source)
475
507
  # - `:timeout => Integer`: timeout (default: same as connection timeout)
508
+ # - `:copy => Boolean`: Do not remove the key from the local instance.
509
+ # - `:replace => Boolean`: Replace existing key on the remote instance.
476
510
  # @return [String] `"OK"`
477
511
  def migrate(key, options)
478
- host = options[:host] || raise(RuntimeError, ":host not specified")
479
- port = options[:port] || raise(RuntimeError, ":port not specified")
480
- db = (options[:db] || client.db).to_i
481
- timeout = (options[:timeout] || client.timeout).to_i
512
+ args = [:migrate]
513
+ args << (options[:host] || raise(':host not specified'))
514
+ args << (options[:port] || raise(':port not specified'))
515
+ args << (key.is_a?(String) ? key : '')
516
+ args << (options[:db] || @client.db).to_i
517
+ args << (options[:timeout] || @client.timeout).to_i
518
+ args << 'COPY' if options[:copy]
519
+ args << 'REPLACE' if options[:replace]
520
+ args += ['KEYS', *key] if key.is_a?(Array)
482
521
 
483
- synchronize do |client|
484
- client.call([:migrate, host, port, key, db, timeout])
485
- end
522
+ synchronize { |client| client.call(args) }
486
523
  end
487
524
 
488
525
  # Delete one or more keys.
@@ -495,6 +532,16 @@ class Redis
495
532
  end
496
533
  end
497
534
 
535
+ # Unlink one or more keys.
536
+ #
537
+ # @param [String, Array<String>] keys
538
+ # @return [Fixnum] number of keys that were unlinked
539
+ def unlink(*keys)
540
+ synchronize do |client|
541
+ client.call([:unlink] + keys)
542
+ end
543
+ end
544
+
498
545
  # Determine if a key exists.
499
546
  #
500
547
  # @param [String] key
@@ -756,8 +803,6 @@ class Redis
756
803
  end
757
804
  end
758
805
 
759
- alias :[]= :set
760
-
761
806
  # Set the time to live in seconds of a key.
762
807
  #
763
808
  # @param [String] key
@@ -863,8 +908,6 @@ class Redis
863
908
  end
864
909
  end
865
910
 
866
- alias :[] :get
867
-
868
911
  # Get the values of all the given keys.
869
912
  #
870
913
  # @example
@@ -1041,7 +1084,7 @@ class Redis
1041
1084
  # Prepend one or more values to a list, creating the list if it doesn't exist
1042
1085
  #
1043
1086
  # @param [String] key
1044
- # @param [String, Array] value string value, or array of string values to push
1087
+ # @param [String, Array<String>] value string value, or array of string values to push
1045
1088
  # @return [Fixnum] the length of the list after the push operation
1046
1089
  def lpush(key, value)
1047
1090
  synchronize do |client|
@@ -1063,7 +1106,7 @@ class Redis
1063
1106
  # Append one or more values to a list, creating the list if it doesn't exist
1064
1107
  #
1065
1108
  # @param [String] key
1066
- # @param [String] value
1109
+ # @param [String, Array<String>] value string value, or array of string values to push
1067
1110
  # @return [Fixnum] the length of the list after the push operation
1068
1111
  def rpush(key, value)
1069
1112
  synchronize do |client|
@@ -1113,15 +1156,14 @@ class Redis
1113
1156
  end
1114
1157
  end
1115
1158
 
1116
- def _bpop(cmd, args)
1159
+ def _bpop(cmd, args, &blk)
1117
1160
  options = {}
1118
1161
 
1119
- case args.last
1120
- when Hash
1162
+ if args.last.is_a?(Hash)
1121
1163
  options = args.pop
1122
- when Integer
1164
+ elsif args.last.respond_to?(:to_int)
1123
1165
  # Issue deprecation notice in obnoxious mode...
1124
- options[:timeout] = args.pop
1166
+ options[:timeout] = args.pop.to_int
1125
1167
  end
1126
1168
 
1127
1169
  if args.size > 1
@@ -1134,7 +1176,7 @@ class Redis
1134
1176
  synchronize do |client|
1135
1177
  command = [cmd, keys, timeout]
1136
1178
  timeout += client.timeout if timeout > 0
1137
- client.call_with_timeout(command, timeout)
1179
+ client.call_with_timeout(command, timeout, &blk)
1138
1180
  end
1139
1181
  end
1140
1182
 
@@ -1585,6 +1627,90 @@ class Redis
1585
1627
  end
1586
1628
  end
1587
1629
 
1630
+ # Removes and returns up to count members with the highest scores in the sorted set stored at key.
1631
+ #
1632
+ # @example Popping a member
1633
+ # redis.zpopmax('zset')
1634
+ # #=> ['b', 2.0]
1635
+ # @example With count option
1636
+ # redis.zpopmax('zset', 2)
1637
+ # #=> [['b', 2.0], ['a', 1.0]]
1638
+ #
1639
+ # @params key [String] a key of the sorted set
1640
+ # @params count [Integer] a number of members
1641
+ #
1642
+ # @return [Array<String, Float>] element and score pair if count is not specified
1643
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1644
+ def zpopmax(key, count = nil)
1645
+ synchronize do |client|
1646
+ members = client.call([:zpopmax, key, count].compact, &FloatifyPairs)
1647
+ count.to_i > 1 ? members : members.first
1648
+ end
1649
+ end
1650
+
1651
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at key.
1652
+ #
1653
+ # @example Popping a member
1654
+ # redis.zpopmin('zset')
1655
+ # #=> ['a', 1.0]
1656
+ # @example With count option
1657
+ # redis.zpopmin('zset', 2)
1658
+ # #=> [['a', 1.0], ['b', 2.0]]
1659
+ #
1660
+ # @params key [String] a key of the sorted set
1661
+ # @params count [Integer] a number of members
1662
+ #
1663
+ # @return [Array<String, Float>] element and score pair if count is not specified
1664
+ # @return [Array<Array<String, Float>>] list of popped elements and scores
1665
+ def zpopmin(key, count = nil)
1666
+ synchronize do |client|
1667
+ members = client.call([:zpopmin, key, count].compact, &FloatifyPairs)
1668
+ count.to_i > 1 ? members : members.first
1669
+ end
1670
+ end
1671
+
1672
+ # Removes and returns up to count members with the highest scores in the sorted set stored at keys,
1673
+ # or block until one is available.
1674
+ #
1675
+ # @example Popping a member from a sorted set
1676
+ # redis.bzpopmax('zset', 1)
1677
+ # #=> ['zset', 'b', 2.0]
1678
+ # @example Popping a member from multiple sorted sets
1679
+ # redis.bzpopmax('zset1', 'zset2', 1)
1680
+ # #=> ['zset1', 'b', 2.0]
1681
+ #
1682
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1683
+ # @params timeout [Integer] the maximum number of seconds to block
1684
+ #
1685
+ # @return [Array<String, String, Float>] a touple of key, member and score
1686
+ # @return [nil] when no element could be popped and the timeout expired
1687
+ def bzpopmax(*args)
1688
+ _bpop(:bzpopmax, args) do |reply|
1689
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1690
+ end
1691
+ end
1692
+
1693
+ # Removes and returns up to count members with the lowest scores in the sorted set stored at keys,
1694
+ # or block until one is available.
1695
+ #
1696
+ # @example Popping a member from a sorted set
1697
+ # redis.bzpopmin('zset', 1)
1698
+ # #=> ['zset', 'a', 1.0]
1699
+ # @example Popping a member from multiple sorted sets
1700
+ # redis.bzpopmin('zset1', 'zset2', 1)
1701
+ # #=> ['zset1', 'a', 1.0]
1702
+ #
1703
+ # @params keys [Array<String>] one or multiple keys of the sorted sets
1704
+ # @params timeout [Integer] the maximum number of seconds to block
1705
+ #
1706
+ # @return [Array<String, String, Float>] a touple of key, member and score
1707
+ # @return [nil] when no element could be popped and the timeout expired
1708
+ def bzpopmin(*args)
1709
+ _bpop(:bzpopmin, args) do |reply|
1710
+ reply.is_a?(Array) ? [reply[0], reply[1], Floatify.call(reply[2])] : reply
1711
+ end
1712
+ end
1713
+
1588
1714
  # Get the score associated with the given member in a sorted set.
1589
1715
  #
1590
1716
  # @example Get the score for member "a"
@@ -1701,6 +1827,30 @@ class Redis
1701
1827
  end
1702
1828
  end
1703
1829
 
1830
+ # Count the members, with the same score in a sorted set, within the given lexicographical range.
1831
+ #
1832
+ # @example Count members matching a
1833
+ # redis.zlexcount("zset", "[a", "[a\xff")
1834
+ # # => 1
1835
+ # @example Count members matching a-z
1836
+ # redis.zlexcount("zset", "[a", "[z\xff")
1837
+ # # => 26
1838
+ #
1839
+ # @param [String] key
1840
+ # @param [String] min
1841
+ # - inclusive minimum is specified by prefixing `(`
1842
+ # - exclusive minimum is specified by prefixing `[`
1843
+ # @param [String] max
1844
+ # - inclusive maximum is specified by prefixing `(`
1845
+ # - exclusive maximum is specified by prefixing `[`
1846
+ #
1847
+ # @return [Fixnum] number of members within the specified lexicographical range
1848
+ def zlexcount(key, min, max)
1849
+ synchronize do |client|
1850
+ client.call([:zlexcount, key, min, max])
1851
+ end
1852
+ end
1853
+
1704
1854
  # Return a range of members with the same score in a sorted set, by lexicographical ordering
1705
1855
  #
1706
1856
  # @example Retrieve members matching a
@@ -2053,9 +2203,9 @@ class Redis
2053
2203
  # @param [String] key
2054
2204
  # @param [String, Array<String>] field
2055
2205
  # @return [Fixnum] the number of fields that were removed from the hash
2056
- def hdel(key, field)
2206
+ def hdel(key, *fields)
2057
2207
  synchronize do |client|
2058
- client.call([:hdel, key, field])
2208
+ client.call([:hdel, key, *fields])
2059
2209
  end
2060
2210
  end
2061
2211
 
@@ -2661,6 +2811,425 @@ class Redis
2661
2811
  end
2662
2812
  end
2663
2813
 
2814
+ # Adds the specified geospatial items (latitude, longitude, name) to the specified key
2815
+ #
2816
+ # @param [String] key
2817
+ # @param [Array] member arguemnts for member or members: longitude, latitude, name
2818
+ # @return [Intger] number of elements added to the sorted set
2819
+ def geoadd(key, *member)
2820
+ synchronize do |client|
2821
+ client.call([:geoadd, key, member])
2822
+ end
2823
+ end
2824
+
2825
+ # Returns geohash string representing position for specified members of the specified key.
2826
+ #
2827
+ # @param [String] key
2828
+ # @param [String, Array<String>] member one member or array of members
2829
+ # @return [Array<String, nil>] returns array containg geohash string if member is present, nil otherwise
2830
+ def geohash(key, member)
2831
+ synchronize do |client|
2832
+ client.call([:geohash, key, member])
2833
+ end
2834
+ end
2835
+
2836
+
2837
+ # Query a sorted set representing a geospatial index to fetch members matching a
2838
+ # given maximum distance from a point
2839
+ #
2840
+ # @param [Array] args key, longitude, latitude, radius, unit(m|km|ft|mi)
2841
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest to the nearest relative to the center
2842
+ # @param [Integer] count limit the results to the first N matching items
2843
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2844
+ # @return [Array<String>] may be changed with `options`
2845
+
2846
+ def georadius(*args, **geoptions)
2847
+ geoarguments = _geoarguments(*args, **geoptions)
2848
+
2849
+ synchronize do |client|
2850
+ client.call([:georadius, *geoarguments])
2851
+ end
2852
+ end
2853
+
2854
+ # Query a sorted set representing a geospatial index to fetch members matching a
2855
+ # given maximum distance from an already existing member
2856
+ #
2857
+ # @param [Array] args key, member, radius, unit(m|km|ft|mi)
2858
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest to the nearest relative to the center
2859
+ # @param [Integer] count limit the results to the first N matching items
2860
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
2861
+ # @return [Array<String>] may be changed with `options`
2862
+
2863
+ def georadiusbymember(*args, **geoptions)
2864
+ geoarguments = _geoarguments(*args, **geoptions)
2865
+
2866
+ synchronize do |client|
2867
+ client.call([:georadiusbymember, *geoarguments])
2868
+ end
2869
+ end
2870
+
2871
+ # Returns longitude and latitude of members of a geospatial index
2872
+ #
2873
+ # @param [String] key
2874
+ # @param [String, Array<String>] member one member or array of members
2875
+ # @return [Array<Array<String>, nil>] returns array of elements, where each element is either array of longitude and latitude or nil
2876
+ def geopos(key, member)
2877
+ synchronize do |client|
2878
+ client.call([:geopos, key, member])
2879
+ end
2880
+ end
2881
+
2882
+ # Returns the distance between two members of a geospatial index
2883
+ #
2884
+ # @param [String ]key
2885
+ # @param [Array<String>] members
2886
+ # @param ['m', 'km', 'mi', 'ft'] unit
2887
+ # @return [String, nil] returns distance in spefied unit if both members present, nil otherwise.
2888
+ def geodist(key, member1, member2, unit = 'm')
2889
+ synchronize do |client|
2890
+ client.call([:geodist, key, member1, member2, unit])
2891
+ end
2892
+ end
2893
+
2894
+ # Returns the stream information each subcommand.
2895
+ #
2896
+ # @example stream
2897
+ # redis.xinfo(:stream, 'mystream')
2898
+ # @example groups
2899
+ # redis.xinfo(:groups, 'mystream')
2900
+ # @example consumers
2901
+ # redis.xinfo(:consumers, 'mystream', 'mygroup')
2902
+ #
2903
+ # @param subcommand [String] e.g. `stream` `groups` `consumers`
2904
+ # @param key [String] the stream key
2905
+ # @param group [String] the consumer group name, required if subcommand is `consumers`
2906
+ #
2907
+ # @return [Hash] information of the stream if subcommand is `stream`
2908
+ # @return [Array<Hash>] information of the consumer groups if subcommand is `groups`
2909
+ # @return [Array<Hash>] information of the consumers if subcommand is `consumers`
2910
+ def xinfo(subcommand, key, group = nil)
2911
+ args = [:xinfo, subcommand, key, group].compact
2912
+ synchronize do |client|
2913
+ client.call(args) do |reply|
2914
+ case subcommand.to_s.downcase
2915
+ when 'stream' then Hashify.call(reply)
2916
+ when 'groups', 'consumers' then reply.map { |arr| Hashify.call(arr) }
2917
+ else reply
2918
+ end
2919
+ end
2920
+ end
2921
+ end
2922
+
2923
+ # Add new entry to the stream.
2924
+ #
2925
+ # @example Without options
2926
+ # redis.xadd('mystream', f1: 'v1', f2: 'v2')
2927
+ # @example With options
2928
+ # redis.xadd('mystream', { f1: 'v1', f2: 'v2' }, id: '0-0', maxlen: 1000, approximate: true)
2929
+ #
2930
+ # @param key [String] the stream key
2931
+ # @param entry [Hash] one or multiple field-value pairs
2932
+ # @param opts [Hash] several options for `XADD` command
2933
+ #
2934
+ # @option opts [String] :id the entry id, default value is `*`, it means auto generation
2935
+ # @option opts [Integer] :maxlen max length of entries
2936
+ # @option opts [Boolean] :approximate whether to add `~` modifier of maxlen or not
2937
+ #
2938
+ # @return [String] the entry id
2939
+ def xadd(key, entry, opts = {})
2940
+ args = [:xadd, key]
2941
+ args.concat(['MAXLEN', (opts[:approximate] ? '~' : nil), opts[:maxlen]].compact) if opts[:maxlen]
2942
+ args << (opts[:id] || '*')
2943
+ args.concat(entry.to_a.flatten)
2944
+ synchronize { |client| client.call(args) }
2945
+ end
2946
+
2947
+ # Trims older entries of the stream if needed.
2948
+ #
2949
+ # @example Without options
2950
+ # redis.xtrim('mystream', 1000)
2951
+ # @example With options
2952
+ # redis.xtrim('mystream', 1000, approximate: true)
2953
+ #
2954
+ # @param key [String] the stream key
2955
+ # @param mexlen [Integer] max length of entries
2956
+ # @param approximate [Boolean] whether to add `~` modifier of maxlen or not
2957
+ #
2958
+ # @return [Integer] the number of entries actually deleted
2959
+ def xtrim(key, maxlen, approximate: false)
2960
+ args = [:xtrim, key, 'MAXLEN', (approximate ? '~' : nil), maxlen].compact
2961
+ synchronize { |client| client.call(args) }
2962
+ end
2963
+
2964
+ # Delete entries by entry ids.
2965
+ #
2966
+ # @example With splatted entry ids
2967
+ # redis.xdel('mystream', '0-1', '0-2')
2968
+ # @example With arrayed entry ids
2969
+ # redis.xdel('mystream', ['0-1', '0-2'])
2970
+ #
2971
+ # @param key [String] the stream key
2972
+ # @param ids [Array<String>] one or multiple entry ids
2973
+ #
2974
+ # @return [Integer] the number of entries actually deleted
2975
+ def xdel(key, *ids)
2976
+ args = [:xdel, key].concat(ids.flatten)
2977
+ synchronize { |client| client.call(args) }
2978
+ end
2979
+
2980
+ # Fetches entries of the stream.
2981
+ #
2982
+ # @example Without options
2983
+ # redis.xrange('mystream')
2984
+ # @example With first entry id option
2985
+ # redis.xrange('mystream', first: '0-1')
2986
+ # @example With first and last entry id options
2987
+ # redis.xrange('mystream', first: '0-1', last: '0-3')
2988
+ # @example With count options
2989
+ # redis.xrange('mystream', count: 10)
2990
+ #
2991
+ # @param key [String] the stream key
2992
+ # @param start [String] first entry id of range, default value is `+`
2993
+ # @param end [String] last entry id of range, default value is `-`
2994
+ # @param count [Integer] the number of entries as limit
2995
+ #
2996
+ # @return [Hash{String => Hash}] the entries
2997
+
2998
+ # Fetches entries of the stream in ascending order.
2999
+ #
3000
+ # @example Without options
3001
+ # redis.xrange('mystream')
3002
+ # @example With a specific start
3003
+ # redis.xrange('mystream', '0-1')
3004
+ # @example With a specific start and end
3005
+ # redis.xrange('mystream', '0-1', '0-3')
3006
+ # @example With count options
3007
+ # redis.xrange('mystream', count: 10)
3008
+ #
3009
+ # @param key [String] the stream key
3010
+ # @param start [String] first entry id of range, default value is `-`
3011
+ # @param end [String] last entry id of range, default value is `+`
3012
+ # @param count [Integer] the number of entries as limit
3013
+ #
3014
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3015
+ def xrange(key, start = '-', _end = '+', count: nil)
3016
+ args = [:xrange, key, start, _end]
3017
+ args.concat(['COUNT', count]) if count
3018
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3019
+ end
3020
+
3021
+ # Fetches entries of the stream in descending order.
3022
+ #
3023
+ # @example Without options
3024
+ # redis.xrevrange('mystream')
3025
+ # @example With a specific end
3026
+ # redis.xrevrange('mystream', '0-3')
3027
+ # @example With a specific end and start
3028
+ # redis.xrevrange('mystream', '0-3', '0-1')
3029
+ # @example With count options
3030
+ # redis.xrevrange('mystream', count: 10)
3031
+ #
3032
+ # @param key [String] the stream key
3033
+ # @param end [String] first entry id of range, default value is `+`
3034
+ # @param start [String] last entry id of range, default value is `-`
3035
+ # @params count [Integer] the number of entries as limit
3036
+ #
3037
+ # @return [Array<Array<String, Hash>>] the ids and entries pairs
3038
+ def xrevrange(key, _end = '+', start = '-', count: nil)
3039
+ args = [:xrevrange, key, _end, start]
3040
+ args.concat(['COUNT', count]) if count
3041
+ synchronize { |client| client.call(args, &HashifyStreamEntries) }
3042
+ end
3043
+
3044
+ # Returns the number of entries inside a stream.
3045
+ #
3046
+ # @example With key
3047
+ # redis.xlen('mystream')
3048
+ #
3049
+ # @param key [String] the stream key
3050
+ #
3051
+ # @return [Integer] the number of entries
3052
+ def xlen(key)
3053
+ synchronize { |client| client.call([:xlen, key]) }
3054
+ end
3055
+
3056
+ # Fetches entries from one or multiple streams. Optionally blocking.
3057
+ #
3058
+ # @example With a key
3059
+ # redis.xread('mystream', '0-0')
3060
+ # @example With multiple keys
3061
+ # redis.xread(%w[mystream1 mystream2], %w[0-0 0-0])
3062
+ # @example With count option
3063
+ # redis.xread('mystream', '0-0', count: 2)
3064
+ # @example With block option
3065
+ # redis.xread('mystream', '$', block: 1000)
3066
+ #
3067
+ # @param keys [Array<String>] one or multiple stream keys
3068
+ # @param ids [Array<String>] one or multiple entry ids
3069
+ # @param count [Integer] the number of entries as limit per stream
3070
+ # @param block [Integer] the number of milliseconds as blocking timeout
3071
+ #
3072
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3073
+ def xread(keys, ids, count: nil, block: nil)
3074
+ args = [:xread]
3075
+ args << 'COUNT' << count if count
3076
+ args << 'BLOCK' << block.to_i if block
3077
+ _xread(args, keys, ids, block)
3078
+ end
3079
+
3080
+ # Manages the consumer group of the stream.
3081
+ #
3082
+ # @example With `create` subcommand
3083
+ # redis.xgroup(:create, 'mystream', 'mygroup', '$')
3084
+ # @example With `setid` subcommand
3085
+ # redis.xgroup(:setid, 'mystream', 'mygroup', '$')
3086
+ # @example With `destroy` subcommand
3087
+ # redis.xgroup(:destroy, 'mystream', 'mygroup')
3088
+ # @example With `delconsumer` subcommand
3089
+ # redis.xgroup(:delconsumer, 'mystream', 'mygroup', 'consumer1')
3090
+ #
3091
+ # @param subcommand [String] `create` `setid` `destroy` `delconsumer`
3092
+ # @param key [String] the stream key
3093
+ # @param group [String] the consumer group name
3094
+ # @param id_or_consumer [String]
3095
+ # * the entry id or `$`, required if subcommand is `create` or `setid`
3096
+ # * the consumer name, required if subcommand is `delconsumer`
3097
+ # @param mkstream [Boolean] whether to create an empty stream automatically or not
3098
+ #
3099
+ # @return [String] `OK` if subcommand is `create` or `setid`
3100
+ # @return [Integer] effected count if subcommand is `destroy` or `delconsumer`
3101
+ def xgroup(subcommand, key, group, id_or_consumer = nil, mkstream: false)
3102
+ args = [:xgroup, subcommand, key, group, id_or_consumer, (mkstream ? 'MKSTREAM' : nil)].compact
3103
+ synchronize { |client| client.call(args) }
3104
+ end
3105
+
3106
+ # Fetches a subset of the entries from one or multiple streams related with the consumer group.
3107
+ # Optionally blocking.
3108
+ #
3109
+ # @example With a key
3110
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>')
3111
+ # @example With multiple keys
3112
+ # redis.xreadgroup('mygroup', 'consumer1', %w[mystream1 mystream2], %w[> >])
3113
+ # @example With count option
3114
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', count: 2)
3115
+ # @example With block option
3116
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', block: 1000)
3117
+ # @example With noack option
3118
+ # redis.xreadgroup('mygroup', 'consumer1', 'mystream', '>', noack: true)
3119
+ #
3120
+ # @param group [String] the consumer group name
3121
+ # @param consumer [String] the consumer name
3122
+ # @param keys [Array<String>] one or multiple stream keys
3123
+ # @param ids [Array<String>] one or multiple entry ids
3124
+ # @param opts [Hash] several options for `XREADGROUP` command
3125
+ #
3126
+ # @option opts [Integer] :count the number of entries as limit
3127
+ # @option opts [Integer] :block the number of milliseconds as blocking timeout
3128
+ # @option opts [Boolean] :noack whether message loss is acceptable or not
3129
+ #
3130
+ # @return [Hash{String => Hash{String => Hash}}] the entries
3131
+ def xreadgroup(group, consumer, keys, ids, opts = {})
3132
+ args = [:xreadgroup, 'GROUP', group, consumer]
3133
+ args << 'COUNT' << opts[:count] if opts[:count]
3134
+ args << 'BLOCK' << opts[:block].to_i if opts[:block]
3135
+ args << 'NOACK' if opts[:noack]
3136
+ _xread(args, keys, ids, opts[:block])
3137
+ end
3138
+
3139
+ # Removes one or multiple entries from the pending entries list of a stream consumer group.
3140
+ #
3141
+ # @example With a entry id
3142
+ # redis.xack('mystream', 'mygroup', '1526569495631-0')
3143
+ # @example With splatted entry ids
3144
+ # redis.xack('mystream', 'mygroup', '0-1', '0-2')
3145
+ # @example With arrayed entry ids
3146
+ # redis.xack('mystream', 'mygroup', %w[0-1 0-2])
3147
+ #
3148
+ # @param key [String] the stream key
3149
+ # @param group [String] the consumer group name
3150
+ # @param ids [Array<String>] one or multiple entry ids
3151
+ #
3152
+ # @return [Integer] the number of entries successfully acknowledged
3153
+ def xack(key, group, *ids)
3154
+ args = [:xack, key, group].concat(ids.flatten)
3155
+ synchronize { |client| client.call(args) }
3156
+ end
3157
+
3158
+ # Changes the ownership of a pending entry
3159
+ #
3160
+ # @example With splatted entry ids
3161
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, '0-1', '0-2')
3162
+ # @example With arrayed entry ids
3163
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2])
3164
+ # @example With idle option
3165
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], idle: 1000)
3166
+ # @example With time option
3167
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], time: 1542866959000)
3168
+ # @example With retrycount option
3169
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], retrycount: 10)
3170
+ # @example With force option
3171
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], force: true)
3172
+ # @example With justid option
3173
+ # redis.xclaim('mystream', 'mygroup', 'consumer1', 3600000, %w[0-1 0-2], justid: true)
3174
+ #
3175
+ # @param key [String] the stream key
3176
+ # @param group [String] the consumer group name
3177
+ # @param consumer [String] the consumer name
3178
+ # @param min_idle_time [Integer] the number of milliseconds
3179
+ # @param ids [Array<String>] one or multiple entry ids
3180
+ # @param opts [Hash] several options for `XCLAIM` command
3181
+ #
3182
+ # @option opts [Integer] :idle the number of milliseconds as last time it was delivered of the entry
3183
+ # @option opts [Integer] :time the number of milliseconds as a specific Unix Epoch time
3184
+ # @option opts [Integer] :retrycount the number of retry counter
3185
+ # @option opts [Boolean] :force whether to create the pending entry to the pending entries list or not
3186
+ # @option opts [Boolean] :justid whether to fetch just an array of entry ids or not
3187
+ #
3188
+ # @return [Hash{String => Hash}] the entries successfully claimed
3189
+ # @return [Array<String>] the entry ids successfully claimed if justid option is `true`
3190
+ def xclaim(key, group, consumer, min_idle_time, *ids, **opts)
3191
+ args = [:xclaim, key, group, consumer, min_idle_time].concat(ids.flatten)
3192
+ args.concat(['IDLE', opts[:idle].to_i]) if opts[:idle]
3193
+ args.concat(['TIME', opts[:time].to_i]) if opts[:time]
3194
+ args.concat(['RETRYCOUNT', opts[:retrycount]]) if opts[:retrycount]
3195
+ args << 'FORCE' if opts[:force]
3196
+ args << 'JUSTID' if opts[:justid]
3197
+ blk = opts[:justid] ? Noop : HashifyStreamEntries
3198
+ synchronize { |client| client.call(args, &blk) }
3199
+ end
3200
+
3201
+ # Fetches not acknowledging pending entries
3202
+ #
3203
+ # @example With key and group
3204
+ # redis.xpending('mystream', 'mygroup')
3205
+ # @example With range options
3206
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10)
3207
+ # @example With range and consumer options
3208
+ # redis.xpending('mystream', 'mygroup', '-', '+', 10, 'consumer1')
3209
+ #
3210
+ # @param key [String] the stream key
3211
+ # @param group [String] the consumer group name
3212
+ # @param start [String] start first entry id of range
3213
+ # @param end [String] end last entry id of range
3214
+ # @param count [Integer] count the number of entries as limit
3215
+ # @param consumer [String] the consumer name
3216
+ #
3217
+ # @return [Hash] the summary of pending entries
3218
+ # @return [Array<Hash>] the pending entries details if options were specified
3219
+ def xpending(key, group, *args)
3220
+ command_args = [:xpending, key, group]
3221
+ case args.size
3222
+ when 0, 3, 4
3223
+ command_args.concat(args)
3224
+ else
3225
+ raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 2, 5 or 6)"
3226
+ end
3227
+
3228
+ summary_needed = args.empty?
3229
+ blk = summary_needed ? HashifyStreamPendings : HashifyStreamPendingDetails
3230
+ synchronize { |client| client.call(command_args, &blk) }
3231
+ end
3232
+
2664
3233
  # Interact with the sentinel command (masters, master, slaves, failover)
2665
3234
  #
2666
3235
  # @param [String] subcommand e.g. `masters`, `master`, `slaves`
@@ -2688,6 +3257,41 @@ class Redis
2688
3257
  end
2689
3258
  end
2690
3259
 
3260
+ # Sends `CLUSTER *` command to random node and returns its reply.
3261
+ #
3262
+ # @see https://redis.io/commands#cluster Reference of cluster command
3263
+ #
3264
+ # @param subcommand [String, Symbol] the subcommand of cluster command
3265
+ # e.g. `:slots`, `:nodes`, `:slaves`, `:info`
3266
+ #
3267
+ # @return [Object] depends on the subcommand
3268
+ def cluster(subcommand, *args)
3269
+ subcommand = subcommand.to_s.downcase
3270
+ block = case subcommand
3271
+ when 'slots' then HashifyClusterSlots
3272
+ when 'nodes' then HashifyClusterNodes
3273
+ when 'slaves' then HashifyClusterSlaves
3274
+ when 'info' then HashifyInfo
3275
+ else Noop
3276
+ end
3277
+
3278
+ # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
3279
+ block = Noop unless @cluster_mode
3280
+
3281
+ synchronize do |client|
3282
+ client.call([:cluster, subcommand] + args, &block)
3283
+ end
3284
+ end
3285
+
3286
+ # Sends `ASKING` command to random node and returns its reply.
3287
+ #
3288
+ # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
3289
+ #
3290
+ # @return [String] `'OK'`
3291
+ def asking
3292
+ synchronize { |client| client.call(%i[asking]) }
3293
+ end
3294
+
2691
3295
  def id
2692
3296
  @original_client.id
2693
3297
  end
@@ -2701,12 +3305,14 @@ class Redis
2701
3305
  end
2702
3306
 
2703
3307
  def connection
3308
+ return @original_client.connection_info if @cluster_mode
3309
+
2704
3310
  {
2705
- :host => @original_client.host,
2706
- :port => @original_client.port,
2707
- :db => @original_client.db,
2708
- :id => @original_client.id,
2709
- :location => @original_client.location
3311
+ host: @original_client.host,
3312
+ port: @original_client.port,
3313
+ db: @original_client.db,
3314
+ id: @original_client.id,
3315
+ location: @original_client.location
2710
3316
  }
2711
3317
  end
2712
3318
 
@@ -2756,14 +3362,107 @@ private
2756
3362
  }
2757
3363
 
2758
3364
  FloatifyPairs =
2759
- lambda { |array|
2760
- if array
2761
- array.each_slice(2).map do |member, score|
2762
- [member, Floatify.call(score)]
2763
- end
3365
+ lambda { |result|
3366
+ result.each_slice(2).map do |member, score|
3367
+ [member, Floatify.call(score)]
3368
+ end
3369
+ }
3370
+
3371
+ HashifyInfo =
3372
+ lambda { |reply|
3373
+ Hash[reply.split("\r\n").map do |line|
3374
+ line.split(':', 2) unless line =~ /^(#|$)/
3375
+ end.compact]
3376
+ }
3377
+
3378
+ HashifyStreams =
3379
+ lambda { |reply|
3380
+ return {} if reply.nil?
3381
+ reply.map do |stream_key, entries|
3382
+ [stream_key, HashifyStreamEntries.call(entries)]
3383
+ end.to_h
3384
+ }
3385
+
3386
+ HashifyStreamEntries =
3387
+ lambda { |reply|
3388
+ reply.map do |entry_id, values|
3389
+ [entry_id, values.each_slice(2).to_h]
2764
3390
  end
2765
3391
  }
2766
3392
 
3393
+ HashifyStreamPendings =
3394
+ lambda { |reply|
3395
+ {
3396
+ 'size' => reply[0],
3397
+ 'min_entry_id' => reply[1],
3398
+ 'max_entry_id' => reply[2],
3399
+ 'consumers' => reply[3].nil? ? {} : Hash[reply[3]]
3400
+ }
3401
+ }
3402
+
3403
+ HashifyStreamPendingDetails =
3404
+ lambda { |reply|
3405
+ reply.map do |arr|
3406
+ {
3407
+ 'entry_id' => arr[0],
3408
+ 'consumer' => arr[1],
3409
+ 'elapsed' => arr[2],
3410
+ 'count' => arr[3]
3411
+ }
3412
+ end
3413
+ }
3414
+
3415
+ HashifyClusterNodeInfo =
3416
+ lambda { |str|
3417
+ arr = str.split(' ')
3418
+ {
3419
+ 'node_id' => arr[0],
3420
+ 'ip_port' => arr[1],
3421
+ 'flags' => arr[2].split(','),
3422
+ 'master_node_id' => arr[3],
3423
+ 'ping_sent' => arr[4],
3424
+ 'pong_recv' => arr[5],
3425
+ 'config_epoch' => arr[6],
3426
+ 'link_state' => arr[7],
3427
+ 'slots' => arr[8].nil? ? nil : Range.new(*arr[8].split('-'))
3428
+ }
3429
+ }
3430
+
3431
+ HashifyClusterSlots =
3432
+ lambda { |reply|
3433
+ reply.map do |arr|
3434
+ first_slot, last_slot = arr[0..1]
3435
+ master = { 'ip' => arr[2][0], 'port' => arr[2][1], 'node_id' => arr[2][2] }
3436
+ replicas = arr[3..-1].map { |r| { 'ip' => r[0], 'port' => r[1], 'node_id' => r[2] } }
3437
+ {
3438
+ 'start_slot' => first_slot,
3439
+ 'end_slot' => last_slot,
3440
+ 'master' => master,
3441
+ 'replicas' => replicas
3442
+ }
3443
+ end
3444
+ }
3445
+
3446
+ HashifyClusterNodes =
3447
+ lambda { |reply|
3448
+ reply.split(/[\r\n]+/).map { |str| HashifyClusterNodeInfo.call(str) }
3449
+ }
3450
+
3451
+ HashifyClusterSlaves =
3452
+ lambda { |reply|
3453
+ reply.map { |str| HashifyClusterNodeInfo.call(str) }
3454
+ }
3455
+
3456
+ Noop = ->(reply) { reply }
3457
+
3458
+ def _geoarguments(*args, options: nil, sort: nil, count: nil)
3459
+ args.push sort if sort
3460
+ args.push 'count', count if count
3461
+ args.push options if options
3462
+
3463
+ args.uniq
3464
+ end
3465
+
2767
3466
  def _subscription(method, timeout, channels, block)
2768
3467
  return @client.call([method] + channels) if subscribed?
2769
3468
 
@@ -2779,10 +3478,27 @@ private
2779
3478
  end
2780
3479
  end
2781
3480
 
3481
+ def _xread(args, keys, ids, blocking_timeout_msec)
3482
+ keys = keys.is_a?(Array) ? keys : [keys]
3483
+ ids = ids.is_a?(Array) ? ids : [ids]
3484
+ args << 'STREAMS'
3485
+ args.concat(keys)
3486
+ args.concat(ids)
3487
+
3488
+ synchronize do |client|
3489
+ if blocking_timeout_msec.nil?
3490
+ client.call(args, &HashifyStreams)
3491
+ else
3492
+ timeout = client.timeout.to_f + blocking_timeout_msec.to_f / 1000.0
3493
+ client.call_with_timeout(args, timeout, &HashifyStreams)
3494
+ end
3495
+ end
3496
+ end
2782
3497
  end
2783
3498
 
2784
- require "redis/version"
2785
- require "redis/connection"
2786
- require "redis/client"
2787
- require "redis/pipeline"
2788
- require "redis/subscribe"
3499
+ require_relative "redis/version"
3500
+ require_relative "redis/connection"
3501
+ require_relative "redis/client"
3502
+ require_relative "redis/cluster"
3503
+ require_relative "redis/pipeline"
3504
+ require_relative "redis/subscribe"