redis 4.4.0 → 4.8.1

Sign up to get free protection for your applications and to get access to all the features.
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