redis 4.4.0 → 4.8.1

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 (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +96 -0
  3. data/README.md +25 -10
  4. data/lib/redis/client.rb +31 -25
  5. data/lib/redis/cluster/command.rb +4 -6
  6. data/lib/redis/cluster/command_loader.rb +8 -9
  7. data/lib/redis/cluster/node.rb +12 -0
  8. data/lib/redis/cluster/node_loader.rb +8 -11
  9. data/lib/redis/cluster/option.rb +10 -3
  10. data/lib/redis/cluster/slot_loader.rb +9 -12
  11. data/lib/redis/cluster.rb +24 -0
  12. data/lib/redis/commands/bitmaps.rb +63 -0
  13. data/lib/redis/commands/cluster.rb +45 -0
  14. data/lib/redis/commands/connection.rb +58 -0
  15. data/lib/redis/commands/geo.rb +84 -0
  16. data/lib/redis/commands/hashes.rb +251 -0
  17. data/lib/redis/commands/hyper_log_log.rb +37 -0
  18. data/lib/redis/commands/keys.rb +455 -0
  19. data/lib/redis/commands/lists.rb +290 -0
  20. data/lib/redis/commands/pubsub.rb +72 -0
  21. data/lib/redis/commands/scripting.rb +114 -0
  22. data/lib/redis/commands/server.rb +188 -0
  23. data/lib/redis/commands/sets.rb +223 -0
  24. data/lib/redis/commands/sorted_sets.rb +812 -0
  25. data/lib/redis/commands/streams.rb +382 -0
  26. data/lib/redis/commands/strings.rb +313 -0
  27. data/lib/redis/commands/transactions.rb +139 -0
  28. data/lib/redis/commands.rb +240 -0
  29. data/lib/redis/connection/command_helper.rb +2 -0
  30. data/lib/redis/connection/hiredis.rb +3 -2
  31. data/lib/redis/connection/ruby.rb +19 -9
  32. data/lib/redis/connection/synchrony.rb +10 -8
  33. data/lib/redis/connection.rb +1 -1
  34. data/lib/redis/distributed.rb +111 -23
  35. data/lib/redis/errors.rb +9 -0
  36. data/lib/redis/pipeline.rb +128 -3
  37. data/lib/redis/version.rb +1 -1
  38. data/lib/redis.rb +138 -3482
  39. metadata +22 -5
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5ff2ee4b6a6f2b087ac26bf96a3c1769cf42f70ea90008361157f7ef04cdb14
4
- data.tar.gz: f2c24654294c4fa81a5cff4ddb545bc59e6f85b64ee61273278cb1e286a4edb8
3
+ metadata.gz: b18788ff80698e8f79fb103e7419d9ba74fb0b6a5eb55c672422cd76abff985c
4
+ data.tar.gz: 1eed18a57039c677c894564ceaa1cf6bc9c0535be51501d078b09d75617a9d89
5
5
  SHA512:
6
- metadata.gz: 0d88c6621659a178dca04d3ca62f25520f5f1a846b6300f93fb008966551fb4795a3e6c37810f99ea18f77a6292b955fa180ae3e06f8c90df7e6a3ca27087ae1
7
- data.tar.gz: 1427cc268e867872388f214184d23a5c6062104fcb9d1e50d6026dd9daf6e51b9833866fee9fcf80c7a64e4de705295de8399d8e8ac22fa890bf358bd63ff707
6
+ metadata.gz: 5f2f7ce595d431f548c126a63c5ce697cc9e596e9b0eaa00f1e0186ae3853e73922c6e130b989ba9e026aec32f766a774433fdd1cff216420e837837db90659b
7
+ data.tar.gz: 02fb8debb0d11f7b9d04f7616093535426f0ee5a0994992fcf3187ab00aa890dcd2fb248f14da49837508a3ccbb5756be208fc5d3b8dcd18ec6aaf0fb1bb1193
data/CHANGELOG.md CHANGED
@@ -1,5 +1,101 @@
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
+
78
+ # 4.5.1
79
+
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,
81
+ redis-rb will first try to connect as the provided user, but then will fallback to connect as the `default` user with the provided password.
82
+ This behavior is deprecated and will be removed in Redis 4.6.0. Fix #1038.
83
+
84
+ # 4.5.0
85
+
86
+ * Handle parts of the command using incompatible encodings. See #1037.
87
+ * Add GET option to SET command. See #1036.
88
+ * Add ZRANDMEMBER command. See #1035.
89
+ * Add LMOVE/BLMOVE commands. See #1034.
90
+ * Add ZMSCORE command. See #1032.
91
+ * Add LT/GT options to ZADD. See #1033.
92
+ * Add SMISMEMBER command. See #1031.
93
+ * Add EXAT/PXAT options to SET. See #1028.
94
+ * Add GETDEL/GETEX commands. See #1024.
95
+ * `Redis#exists` now returns an Integer by default, as warned since 4.2.0. The old behavior can be restored with `Redis.exists_returns_integer = false`.
96
+ * Fix Redis < 6 detection during connect. See #1025.
97
+ * Fix fetching command details in Redis cluster when the first node is unhealthy. See #1026.
98
+
3
99
  # 4.4.0
4
100
 
5
101
  * Redis cluster: fix cross-slot validation in pipelines. Fix ##1019.
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)
@@ -119,8 +117,23 @@ class Redis
119
117
  if username
120
118
  begin
121
119
  call [:auth, username, password]
122
- rescue CommandError # Likely on Redis < 6
123
- call [:auth, password]
120
+ rescue CommandError => err # Likely on Redis < 6
121
+ case err.message
122
+ when /ERR wrong number of arguments for 'auth' command/
123
+ call [:auth, password]
124
+ when /WRONGPASS invalid username-password pair/
125
+ begin
126
+ call [:auth, password]
127
+ rescue CommandError
128
+ raise err
129
+ end
130
+ ::Redis.deprecate!(
131
+ "[redis-rb] The Redis connection was configured with username #{username.inspect}, but" \
132
+ " the provided password was for the default user. This will start failing in redis-rb 5.0.0."
133
+ )
134
+ else
135
+ raise
136
+ end
124
137
  end
125
138
  else
126
139
  call [:auth, password]
@@ -137,7 +150,7 @@ class Redis
137
150
  end
138
151
 
139
152
  def id
140
- @options[:id] || "redis://#{location}/#{db}"
153
+ @options[:id] || "#{@options[:ssl] ? 'rediss' : @options[:scheme]}://#{location}/#{db}"
141
154
  end
142
155
 
143
156
  def location
@@ -238,7 +251,8 @@ class Redis
238
251
  result
239
252
  end
240
253
 
241
- 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
242
256
  with_socket_timeout(timeout) do
243
257
  call(command, &blk)
244
258
  end
@@ -288,7 +302,7 @@ class Redis
288
302
  e2 = TimeoutError.new("Connection timed out")
289
303
  e2.set_backtrace(e1.backtrace)
290
304
  raise e2
291
- 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
292
306
  raise ConnectionError, "Connection lost (%s)" % [e.class.name.split("::").last]
293
307
  end
294
308
 
@@ -385,23 +399,14 @@ class Redis
385
399
  end
386
400
 
387
401
  def ensure_connected
388
- disconnect if @pending_reads > 0
402
+ disconnect if @pending_reads > 0 || (@pid != Process.pid && !inherit_socket?)
389
403
 
390
404
  attempts = 0
391
405
 
392
406
  begin
393
407
  attempts += 1
394
408
 
395
- if connected?
396
- unless inherit_socket? || Process.pid == @pid
397
- raise InheritedError,
398
- "Tried to use a connection from a child process without reconnecting. " \
399
- "You need to reconnect to Redis after forking " \
400
- "or set :inherit_socket to true."
401
- end
402
- else
403
- connect
404
- end
409
+ connect unless connected?
405
410
 
406
411
  yield
407
412
  rescue BaseConnectionError
@@ -428,7 +433,7 @@ class Redis
428
433
  defaults = DEFAULTS.dup
429
434
  options = options.dup
430
435
 
431
- defaults.keys.each do |key|
436
+ defaults.each_key do |key|
432
437
  # Fill in defaults if needed
433
438
  defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
434
439
 
@@ -445,11 +450,12 @@ class Redis
445
450
 
446
451
  uri = URI(url)
447
452
 
448
- if uri.scheme == "unix"
453
+ case uri.scheme
454
+ when "unix"
449
455
  defaults[:path] = uri.path
450
- elsif uri.scheme == "redis" || uri.scheme == "rediss"
456
+ when "redis", "rediss"
451
457
  defaults[:scheme] = uri.scheme
452
- defaults[:host] = uri.host if uri.host
458
+ defaults[:host] = uri.host.sub(/\A\[(.*)\]\z/, '\1') if uri.host
453
459
  defaults[:port] = uri.port if uri.port
454
460
  defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
455
461
  defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
@@ -463,7 +469,7 @@ class Redis
463
469
  end
464
470
 
465
471
  # Use default when option is not specified or nil
466
- defaults.keys.each do |key|
472
+ defaults.each_key do |key|
467
473
  options[key] = defaults[key] if options[key].nil?
468
474
  end
469
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,22 +10,21 @@ class Redis
10
10
  module_function
11
11
 
12
12
  def load(nodes)
13
- details = {}
14
-
15
- nodes.each do |node|
16
- details = fetch_command_details(node)
17
- details.empty? ? next : break
13
+ errors = nodes.map do |node|
14
+ begin
15
+ return fetch_command_details(node)
16
+ rescue CannotConnectError, ConnectionError, CommandError => error
17
+ error
18
+ end
18
19
  end
19
20
 
20
- details
21
+ raise InitialSetupError, errors
21
22
  end
22
23
 
23
24
  def fetch_command_details(node)
24
25
  node.call(%i[command]).map do |reply|
25
26
  [reply[0], { arity: reply[1], flags: reply[2], first: reply[3], last: reply[4], step: reply[5] }]
26
27
  end.to_h
27
- rescue CannotConnectError, ConnectionError, CommandError
28
- {} # can retry on another node
29
28
  end
30
29
 
31
30
  private_class_method :fetch_command_details
@@ -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