redis 3.3.5 → 4.1.0

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