redis 4.5.1 → 4.8.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a7232fef186e6d6a11a90d8dd9aa7c71114f017e0afe9378999a96c9e6b4e05
4
- data.tar.gz: 689f176b87909bf61eb60e57d1eb795198162a7e039104c80facf36880964bda
3
+ metadata.gz: b18788ff80698e8f79fb103e7419d9ba74fb0b6a5eb55c672422cd76abff985c
4
+ data.tar.gz: 1eed18a57039c677c894564ceaa1cf6bc9c0535be51501d078b09d75617a9d89
5
5
  SHA512:
6
- metadata.gz: 22b11dee92e298b46bb94a707156d3dbf9afb83c8e9e8cbf82cf366d582a7c1b7295d7f09a0fec01965245f4800c2482c0559d646f46ff1c6bba6423ab398ba9
7
- data.tar.gz: 9a74ba29c8cb3d7634a44c78e30018a048e19cca66c7fad6a226722183f66546bebb370c92ecdf20522bb35ee72a09b2fda64891ff6d94db1702375bd1ba6b46
6
+ metadata.gz: 5f2f7ce595d431f548c126a63c5ce697cc9e596e9b0eaa00f1e0186ae3853e73922c6e130b989ba9e026aec32f766a774433fdd1cff216420e837837db90659b
7
+ data.tar.gz: 02fb8debb0d11f7b9d04f7616093535426f0ee5a0994992fcf3187ab00aa890dcd2fb248f14da49837508a3ccbb5756be208fc5d3b8dcd18ec6aaf0fb1bb1193
data/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
1
  # Unreleased
2
2
 
3
+ # 4.8.1
4
+
5
+ * Automatically reconnect after fork regardless of `reconnect_attempts`
6
+
7
+ # 4.8.0
8
+
9
+ * Introduce `sadd?` and `srem?` as boolean returning versions of `sadd` and `srem`.
10
+ * Deprecate `sadd` and `srem` returning a boolean when called with a single argument.
11
+ To enable the redis 5.0 behavior you can set `Redis.sadd_returns_boolean = false`.
12
+ * Deprecate passing `timeout` as a positional argument in blocking commands (`brpop`, `blop`, etc).
13
+
14
+ # 4.7.1
15
+
16
+ * Gracefully handle OpenSSL 3.0 EOF Errors (`OpenSSL::SSL::SSLError: SSL_read: unexpected eof while reading`). See #1106
17
+ This happens frequently on heroku-22.
18
+
19
+ # 4.7.0
20
+
21
+ * Support single endpoint architecture with SSL/TLS in cluster mode. See #1086.
22
+ * `zrem` and `zadd` act as noop when provided an empty list of keys. See #1097.
23
+ * Support IPv6 URLs.
24
+ * Add `Redis#with` for better compatibility with `connection_pool` usage.
25
+ * Fix the block form of `multi` called inside `pipelined`. Previously the `MUTLI/EXEC` wouldn't be sent. See #1073.
26
+
27
+ # 4.6.0
28
+
29
+ * Deprecate `Redis.current`.
30
+ * Deprecate calling commands on `Redis` inside `Redis#pipelined`. See #1059.
31
+ ```ruby
32
+ redis.pipelined do
33
+ redis.get("key")
34
+ end
35
+ ```
36
+
37
+ should be replaced by:
38
+
39
+ ```ruby
40
+ redis.pipelined do |pipeline|
41
+ pipeline.get("key")
42
+ end
43
+ ```
44
+ * Deprecate calling commands on `Redis` inside `Redis#multi`. See #1059.
45
+ ```ruby
46
+ redis.multi do
47
+ redis.get("key")
48
+ end
49
+ ```
50
+
51
+ should be replaced by:
52
+
53
+ ```ruby
54
+ redis.multi do |transaction|
55
+ transaction.get("key")
56
+ end
57
+ ```
58
+ * Deprecate `Redis#queue` and `Redis#commit`. See #1059.
59
+
60
+ * Fix `zpopmax` and `zpopmin` when called inside a pipeline. See #1055.
61
+ * `Redis#synchronize` is now private like it should always have been.
62
+
63
+ * Add `Redis.silence_deprecations=` to turn off deprecation warnings.
64
+ If you don't wish to see warnings yet, you can set `Redis.silence_deprecations = true`.
65
+ It is however heavily recommended to fix them instead when possible.
66
+ * Add `Redis.raise_deprecations=` to turn deprecation warnings into errors.
67
+ This makes it easier to identitify the source of deprecated APIs usage.
68
+ It is recommended to set `Redis.raise_deprecations = true` in development and test environments.
69
+ * Add new options to ZRANGE. See #1053.
70
+ * Add ZRANGESTORE command. See #1053.
71
+ * Add SCAN support for `Redis::Cluster`. See #1049.
72
+ * Add COPY command. See #1053. See #1048.
73
+ * Add ZDIFFSTORE command. See #1046.
74
+ * Add ZDIFF command. See #1044.
75
+ * Add ZUNION command. See #1042.
76
+ * Add HRANDFIELD command. See #1040.
77
+
3
78
  # 4.5.1
4
79
 
5
80
  * Restore the accidential auth behavior of redis-rb 4.3.0 with a warning. If provided with the `default` user's password, but a wrong username,
data/README.md CHANGED
@@ -155,6 +155,21 @@ redis.mget('{key}1', '{key}2')
155
155
  * The client support permanent node failures, and will reroute requests to promoted slaves.
156
156
  * The client supports `MOVED` and `ASK` redirections transparently.
157
157
 
158
+ ## Cluster mode with SSL/TLS
159
+ Since Redis can return FQDN of nodes in reply to client since `7.*` with CLUSTER commands, we can use cluster feature with SSL/TLS connection like this:
160
+
161
+ ```ruby
162
+ Redis.new(cluster: %w[rediss://foo.example.com:6379])
163
+ ```
164
+
165
+ On the other hand, in Redis versions prior to `6.*`, you can specify options like the following if cluster mode is enabled and client has to connect to nodes via single endpoint with SSL/TLS.
166
+
167
+ ```ruby
168
+ Redis.new(cluster: %w[rediss://foo-endpoint.example.com:6379], fixed_hostname: 'foo-endpoint.example.com')
169
+ ```
170
+
171
+ In case of the above architecture, if you don't pass the `fixed_hostname` option to the client and servers return IP addresses of nodes, the client may fail to verify certificates.
172
+
158
173
  ## Storing objects
159
174
 
160
175
  Redis "string" types can be used to store serialized Ruby objects, for
@@ -184,9 +199,9 @@ commands to Redis and gathers their replies. These replies are returned
184
199
  by the `#pipelined` method.
185
200
 
186
201
  ```ruby
187
- redis.pipelined do
188
- redis.set "foo", "bar"
189
- redis.incr "baz"
202
+ redis.pipelined do |pipeline|
203
+ pipeline.set "foo", "bar"
204
+ pipeline.incr "baz"
190
205
  end
191
206
  # => ["OK", 1]
192
207
  ```
@@ -200,9 +215,9 @@ the regular pipeline, the replies to the commands are returned by the
200
215
  `#multi` method.
201
216
 
202
217
  ```ruby
203
- redis.multi do
204
- redis.set "foo", "bar"
205
- redis.incr "baz"
218
+ redis.multi do |transaction|
219
+ transaction.set "foo", "bar"
220
+ transaction.incr "baz"
206
221
  end
207
222
  # => ["OK", 1]
208
223
  ```
@@ -210,15 +225,15 @@ end
210
225
  ### Futures
211
226
 
212
227
  Replies to commands in a pipeline can be accessed via the *futures* they
213
- emit (since redis-rb 3.0). All calls inside a pipeline block return a
228
+ emit (since redis-rb 3.0). All calls on the pipeline object return a
214
229
  `Future` object, which responds to the `#value` method. When the
215
230
  pipeline has successfully executed, all futures are assigned their
216
231
  respective replies and can be used.
217
232
 
218
233
  ```ruby
219
- redis.pipelined do
220
- @set = redis.set "foo", "bar"
221
- @incr = redis.incr "baz"
234
+ redis.pipelined do |pipeline|
235
+ @set = pipeline.set "foo", "bar"
236
+ @incr = pipeline.incr "baz"
222
237
  end
223
238
 
224
239
  @set.value
data/lib/redis/client.rb CHANGED
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "errors"
4
3
  require "socket"
5
4
  require "cgi"
5
+ require "redis/errors"
6
6
 
7
7
  class Redis
8
8
  class Client
@@ -32,7 +32,7 @@ class Redis
32
32
  role: nil
33
33
  }.freeze
34
34
 
35
- attr_reader :options
35
+ attr_reader :options, :connection, :command_map
36
36
 
37
37
  def scheme
38
38
  @options[:scheme]
@@ -87,8 +87,6 @@ class Redis
87
87
  end
88
88
 
89
89
  attr_accessor :logger
90
- attr_reader :connection
91
- attr_reader :command_map
92
90
 
93
91
  def initialize(options = {})
94
92
  @options = _parse_options(options)
@@ -120,17 +118,18 @@ class Redis
120
118
  begin
121
119
  call [:auth, username, password]
122
120
  rescue CommandError => err # Likely on Redis < 6
123
- if err.message.match?(/ERR wrong number of arguments for \'auth\' command/)
121
+ case err.message
122
+ when /ERR wrong number of arguments for 'auth' command/
124
123
  call [:auth, password]
125
- elsif err.message.match?(/WRONGPASS invalid username-password pair/)
124
+ when /WRONGPASS invalid username-password pair/
126
125
  begin
127
126
  call [:auth, password]
128
127
  rescue CommandError
129
128
  raise err
130
129
  end
131
- ::Kernel.warn(
130
+ ::Redis.deprecate!(
132
131
  "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
133
- " the provided password was for the default user. This will start failing in redis-rb 4.6."
132
+ " the provided password was for the default user. This will start failing in redis-rb 5.0.0."
134
133
  )
135
134
  else
136
135
  raise
@@ -151,7 +150,7 @@ class Redis
151
150
  end
152
151
 
153
152
  def id
154
- @options[:id] || "redis://#{location}/#{db}"
153
+ @options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
155
154
  end
156
155
 
157
156
  def location
@@ -252,7 +251,8 @@ class Redis
252
251
  result
253
252
  end
254
253
 
255
- def call_with_timeout(command, timeout, &blk)
254
+ def call_with_timeout(command, extra_timeout, &blk)
255
+ timeout = extra_timeout == 0 ? 0 : self.timeout + extra_timeout
256
256
  with_socket_timeout(timeout) do
257
257
  call(command, &blk)
258
258
  end
@@ -302,7 +302,7 @@ class Redis
302
302
  e2 = TimeoutError.new("Connection timed out")
303
303
  e2.set_backtrace(e1.backtrace)
304
304
  raise e2
305
- rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL => e
305
+ rescue Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED, Errno::EBADF, Errno::EINVAL, EOFError => e
306
306
  raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
307
307
  end
308
308
 
@@ -399,23 +399,14 @@ class Redis
399
399
  end
400
400
 
401
401
  def ensure_connected
402
- disconnect if @pending_reads > 0
402
+ disconnect if @pending_reads > 0 || (@pid != Process.pid && !inherit_socket?)
403
403
 
404
404
  attempts = 0
405
405
 
406
406
  begin
407
407
  attempts += 1
408
408
 
409
- if connected?
410
- unless inherit_socket? || Process.pid == @pid
411
- raise InheritedError,
412
- "Tried to use a connection from a child process without reconnecting. " \
413
- "You need to reconnect to Redis after forking " \
414
- "or set :inherit_socket to true."
415
- end
416
- else
417
- connect
418
- end
409
+ connect unless connected?
419
410
 
420
411
  yield
421
412
  rescue BaseConnectionError
@@ -442,7 +433,7 @@ class Redis
442
433
  defaults = DEFAULTS.dup
443
434
  options = options.dup
444
435
 
445
- defaults.keys.each do |key|
436
+ defaults.each_key do |key|
446
437
  # Fill in defaults if needed
447
438
  defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
448
439
 
@@ -459,11 +450,12 @@ class Redis
459
450
 
460
451
  uri = URI(url)
461
452
 
462
- if uri.scheme == "unix"
453
+ case uri.scheme
454
+ when "unix"
463
455
  defaults[:path] = uri.path
464
- elsif uri.scheme == "redis" || uri.scheme == "rediss"
456
+ when "redis", "rediss"
465
457
  defaults[:scheme] = uri.scheme
466
- defaults[:host] = uri.host if uri.host
458
+ defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
467
459
  defaults[:port] = uri.port if uri.port
468
460
  defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
469
461
  defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
@@ -477,7 +469,7 @@ class Redis
477
469
  end
478
470
 
479
471
  # Use default when option is not specified or nil
480
- defaults.keys.each do |key|
472
+ defaults.each_key do |key|
481
473
  options[key] = defaults[key] if options[key].nil?
482
474
  end
483
475
 
@@ -31,13 +31,13 @@ class Redis
31
31
  private
32
32
 
33
33
  def pick_details(details)
34
- details.map do |command, detail|
35
- [command, {
34
+ details.transform_values do |detail|
35
+ {
36
36
  first_key_position: detail[:first],
37
37
  write: detail[:flags].include?('write'),
38
38
  readonly: detail[:flags].include?('readonly')
39
- }]
40
- end.to_h
39
+ }
40
+ end
41
41
  end
42
42
 
43
43
  def dig_details(command, key)
@@ -53,8 +53,6 @@ class Redis
53
53
  when 'object' then 2
54
54
  when 'memory'
55
55
  command[1].to_s.casecmp('usage').zero? ? 2 : 0
56
- when 'scan', 'sscan', 'hscan', 'zscan'
57
- determine_optional_key_position(command, 'match')
58
56
  when 'xread', 'xreadgroup'
59
57
  determine_optional_key_position(command, 'streams')
60
58
  else
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../errors'
3
+ require 'redis/errors'
4
4
 
5
5
  class Redis
6
6
  class Cluster
@@ -10,15 +10,15 @@ class Redis
10
10
  module_function
11
11
 
12
12
  def load(nodes)
13
- nodes.each do |node|
13
+ errors = nodes.map do |node|
14
14
  begin
15
15
  return fetch_command_details(node)
16
- rescue CannotConnectError, ConnectionError, CommandError
17
- next # can retry on another node
16
+ rescue CannotConnectError, ConnectionError, CommandError => error
17
+ error
18
18
  end
19
19
  end
20
20
 
21
- raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
21
+ raise InitialSetupError, errors
22
22
  end
23
23
 
24
24
  def fetch_command_details(node)
@@ -58,6 +58,18 @@ class Redis
58
58
  try_map { |_, client| client.process(commands, &block) }.values
59
59
  end
60
60
 
61
+ def scale_reading_clients
62
+ reading_clients = []
63
+
64
+ @clients.each do |node_key, client|
65
+ next unless replica_disabled? ? master?(node_key) : slave?(node_key)
66
+
67
+ reading_clients << client
68
+ end
69
+
70
+ reading_clients
71
+ end
72
+
61
73
  private
62
74
 
63
75
  def replica_disabled?
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../errors'
3
+ require 'redis/errors'
4
4
 
5
5
  class Redis
6
6
  class Cluster
@@ -9,16 +9,15 @@ class Redis
9
9
  module_function
10
10
 
11
11
  def load_flags(nodes)
12
- info = {}
13
-
14
- nodes.each do |node|
15
- info = fetch_node_info(node)
16
- info.empty? ? next : break
12
+ errors = nodes.map do |node|
13
+ begin
14
+ return fetch_node_info(node)
15
+ rescue CannotConnectError, ConnectionError, CommandError => error
16
+ error
17
+ end
17
18
  end
18
19
 
19
- return info unless info.empty?
20
-
21
- raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
20
+ raise InitialSetupError, errors
22
21
  end
23
22
 
24
23
  def fetch_node_info(node)
@@ -27,8 +26,6 @@ class Redis
27
26
  .map { |str| str.split(' ') }
28
27
  .map { |arr| [arr[1].split('@').first, (arr[2].split(',') & %w[master slave]).first] }
29
28
  .to_h
30
- rescue CannotConnectError, ConnectionError, CommandError
31
- {} # can retry on another node
32
29
  end
33
30
 
34
31
  private_class_method :fetch_node_info
@@ -17,6 +17,7 @@ class Redis
17
17
  node_addrs = options.delete(:cluster)
18
18
  @node_opts = build_node_options(node_addrs)
19
19
  @replica = options.delete(:replica) == true
20
+ @fixed_hostname = options.delete(:fixed_hostname)
20
21
  add_common_node_option_if_needed(options, @node_opts, :scheme)
21
22
  add_common_node_option_if_needed(options, @node_opts, :username)
22
23
  add_common_node_option_if_needed(options, @node_opts, :password)
@@ -24,8 +25,12 @@ class Redis
24
25
  end
25
26
 
26
27
  def per_node_key
27
- @node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
28
- .to_h
28
+ @node_opts.map do |opt|
29
+ node_key = NodeKey.build_from_host_port(opt[:host], opt[:port])
30
+ options = @options.merge(opt)
31
+ options = options.merge(host: @fixed_hostname) if @fixed_hostname && !@fixed_hostname.empty?
32
+ [node_key, options]
33
+ end.to_h
29
34
  end
30
35
 
31
36
  def use_replica?
@@ -64,8 +69,10 @@ class Redis
64
69
  raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
65
70
 
66
71
  db = uri.path.split('/')[1]&.to_i
72
+ username = uri.user ? URI.decode_www_form_component(uri.user) : nil
73
+ password = uri.password ? URI.decode_www_form_component(uri.password) : nil
67
74
 
68
- { scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
75
+ { scheme: uri.scheme, username: username, password: password, host: uri.host, port: uri.port, db: db }
69
76
  .reject { |_, v| v.nil? || v == '' }
70
77
  rescue URI::InvalidURIError => err
71
78
  raise InvalidClientOptionError, err.message
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../errors'
4
- require_relative 'node_key'
3
+ require 'redis/errors'
4
+ require 'redis/cluster/node_key'
5
5
 
6
6
  class Redis
7
7
  class Cluster
@@ -10,16 +10,15 @@ class Redis
10
10
  module_function
11
11
 
12
12
  def load(nodes)
13
- info = {}
14
-
15
- nodes.each do |node|
16
- info = fetch_slot_info(node)
17
- info.empty? ? next : break
13
+ errors = nodes.map do |node|
14
+ begin
15
+ return fetch_slot_info(node)
16
+ rescue CannotConnectError, ConnectionError, CommandError => error
17
+ error
18
+ end
18
19
  end
19
20
 
20
- return info unless info.empty?
21
-
22
- raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
21
+ raise InitialSetupError, errors
23
22
  end
24
23
 
25
24
  def fetch_slot_info(node)
@@ -27,8 +26,6 @@ class Redis
27
26
  node.call(%i[cluster slots])
28
27
  .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
29
28
  .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
30
- rescue CannotConnectError, ConnectionError, CommandError
31
- {} # can retry on another node
32
29
  end
33
30
 
34
31
  def parse_slot_info(arr, default_ip:)
data/lib/redis/cluster.rb CHANGED
@@ -137,6 +137,7 @@ class Redis
137
137
  when 'wait' then @node.call_master(command, &block).reduce(:+)
138
138
  when 'keys' then @node.call_slave(command, &block).flatten.sort
139
139
  when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
140
+ when 'scan' then _scan(command, &block)
140
141
  when 'lastsave' then @node.call_all(command, &block).sort
141
142
  when 'role' then @node.call_all(command, &block)
142
143
  when 'config' then send_config_command(command, &block)
@@ -238,6 +239,29 @@ class Redis
238
239
  raise
239
240
  end
240
241
 
242
+ def _scan(command, &block)
243
+ input_cursor = Integer(command[1])
244
+
245
+ client_index = input_cursor % 256
246
+ raw_cursor = input_cursor >> 8
247
+
248
+ clients = @node.scale_reading_clients
249
+
250
+ client = clients[client_index]
251
+ return ['0', []] unless client
252
+
253
+ command[1] = raw_cursor.to_s
254
+
255
+ result_cursor, result_keys = client.call(command, &block)
256
+ result_cursor = Integer(result_cursor)
257
+
258
+ if result_cursor == 0
259
+ client_index += 1
260
+ end
261
+
262
+ [((result_cursor << 8) + client_index).to_s, result_keys]
263
+ end
264
+
241
265
  def assign_redirection_node(err_msg)
242
266
  _, slot, node_key = err_msg.split(' ')
243
267
  slot = slot.to_i
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Commands
5
+ module Bitmaps
6
+ # Sets or clears the bit at offset in the string value stored at key.
7
+ #
8
+ # @param [String] key
9
+ # @param [Integer] offset bit offset
10
+ # @param [Integer] value bit value `0` or `1`
11
+ # @return [Integer] the original bit value stored at `offset`
12
+ def setbit(key, offset, value)
13
+ send_command([:setbit, key, offset, value])
14
+ end
15
+
16
+ # Returns the bit value at offset in the string value stored at key.
17
+ #
18
+ # @param [String] key
19
+ # @param [Integer] offset bit offset
20
+ # @return [Integer] `0` or `1`
21
+ def getbit(key, offset)
22
+ send_command([:getbit, key, offset])
23
+ end
24
+
25
+ # Count the number of set bits in a range of the string value stored at key.
26
+ #
27
+ # @param [String] key
28
+ # @param [Integer] start start index
29
+ # @param [Integer] stop stop index
30
+ # @return [Integer] the number of bits set to 1
31
+ def bitcount(key, start = 0, stop = -1)
32
+ send_command([:bitcount, key, start, stop])
33
+ end
34
+
35
+ # Perform a bitwise operation between strings and store the resulting string in a key.
36
+ #
37
+ # @param [String] operation e.g. `and`, `or`, `xor`, `not`
38
+ # @param [String] destkey destination key
39
+ # @param [String, Array<String>] keys one or more source keys to perform `operation`
40
+ # @return [Integer] the length of the string stored in `destkey`
41
+ def bitop(operation, destkey, *keys)
42
+ send_command([:bitop, operation, destkey, *keys])
43
+ end
44
+
45
+ # Return the position of the first bit set to 1 or 0 in a string.
46
+ #
47
+ # @param [String] key
48
+ # @param [Integer] bit whether to look for the first 1 or 0 bit
49
+ # @param [Integer] start start index
50
+ # @param [Integer] stop stop index
51
+ # @return [Integer] the position of the first 1/0 bit.
52
+ # -1 if looking for 1 and it is not found or start and stop are given.
53
+ def bitpos(key, bit, start = nil, stop = nil)
54
+ raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
55
+
56
+ command = [:bitpos, key, bit]
57
+ command << start if start
58
+ command << stop if stop
59
+ send_command(command)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Commands
5
+ module Cluster
6
+ # Sends `CLUSTER *` command to random node and returns its reply.
7
+ #
8
+ # @see https://redis.io/commands#cluster Reference of cluster command
9
+ #
10
+ # @param subcommand [String, Symbol] the subcommand of cluster command
11
+ # e.g. `:slots`, `:nodes`, `:slaves`, `:info`
12
+ #
13
+ # @return [Object] depends on the subcommand
14
+ def cluster(subcommand, *args)
15
+ subcommand = subcommand.to_s.downcase
16
+ block = case subcommand
17
+ when 'slots'
18
+ HashifyClusterSlots
19
+ when 'nodes'
20
+ HashifyClusterNodes
21
+ when 'slaves'
22
+ HashifyClusterSlaves
23
+ when 'info'
24
+ HashifyInfo
25
+ else
26
+ Noop
27
+ end
28
+
29
+ # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
30
+ block = Noop unless @cluster_mode
31
+
32
+ send_command([:cluster, subcommand] + args, &block)
33
+ end
34
+
35
+ # Sends `ASKING` command to random node and returns its reply.
36
+ #
37
+ # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
38
+ #
39
+ # @return [String] `'OK'`
40
+ def asking
41
+ send_command(%i[asking])
42
+ end
43
+ end
44
+ end
45
+ end