redis 4.2.0 → 4.6.0

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: 9796f6646b7d3aaeeb5ef37629fb1a43422285724d8d2901a219ef4f2882eff5
4
- data.tar.gz: b89f4f1d6a3c9ee93202ce08cdcd3ed184694b5693f7461be7cf7e517139a278
3
+ metadata.gz: 7e99f4e628112a227719d2000dc5b081893273cfbd51ae25bf00a8f6b6594061
4
+ data.tar.gz: 42a8e0cf75aebbc14cdf680b4bc1f7bbdec9e20b53f150c6443c01126960a959
5
5
  SHA512:
6
- metadata.gz: be709e1aad1acee8d7c3e121e946060ce9b693ac80e25af200e2a579988f3952d6522cb9855917dc76628ae2038365bb8754b49a7cc6e90395706ba30fc87a86
7
- data.tar.gz: 00102b01b4b37daab76fc90990f980a4710bec0e797a70d65a7544ef304f01f9b9babbdcc680da71513dd8f79649bd5f529e6804e71297b26978625bea746367
6
+ metadata.gz: 8bc57fe306c601f27d32df4940d5632e9e7d75529f6ac5d42732b21fe04452a4a1ab89567492c6fb7249ef4d69ebb2c0b7c985e18b0ef4f9ee6fb816c31bcbda
7
+ data.tar.gz: 42b2f8c584d6f96d0fc783d0b36af0fd9415d1dbd43ad8fe7a980688e52fbdb51645a07960d9cb3faacf6696c460cf38a013e290d0b58d4bbbb07626f18f15c7
data/CHANGELOG.md CHANGED
@@ -1,5 +1,118 @@
1
1
  # Unreleased
2
2
 
3
+ # 4.6.0
4
+
5
+ * Deprecate `Redis.current`.
6
+ * Deprecate calling commands on `Redis` inside `Redis#pipelined`. See #1059.
7
+ ```ruby
8
+ redis.pipelined do
9
+ redis.get("key")
10
+ end
11
+ ```
12
+
13
+ should be replaced by:
14
+
15
+ ```ruby
16
+ redis.pipelined do |pipeline|
17
+ pipeline.get("key")
18
+ end
19
+ ```
20
+ * Deprecate calling commands on `Redis` inside `Redis#multi`. See #1059.
21
+ ```ruby
22
+ redis.multi do
23
+ redis.get("key")
24
+ end
25
+ ```
26
+
27
+ should be replaced by:
28
+
29
+ ```ruby
30
+ redis.multi do |transaction|
31
+ transaction.get("key")
32
+ end
33
+ ```
34
+ * Deprecate `Redis#queue` and `Redis#commit`. See #1059.
35
+
36
+ * Fix `zpopmax` and `zpopmin` when called inside a pipeline. See #1055.
37
+ * `Redis#synchronize` is now private like it should always have been.
38
+
39
+ * Add `Redis.silence_deprecations=` to turn off deprecation warnings.
40
+ If you don't wish to see warnings yet, you can set `Redis.silence_deprecations = false`.
41
+ It is however heavily recommended to fix them instead when possible.
42
+ * Add `Redis.raise_deprecations=` to turn deprecation warnings into errors.
43
+ This makes it easier to identitify the source of deprecated APIs usage.
44
+ It is recommended to set `Redis.raise_deprecations = true` in development and test environments.
45
+ * Add new options to ZRANGE. See #1053.
46
+ * Add ZRANGESTORE command. See #1053.
47
+ * Add SCAN support for `Redis::Cluster`. See #1049.
48
+ * Add COPY command. See #1053. See #1048.
49
+ * Add ZDIFFSTORE command. See #1046.
50
+ * Add ZDIFF command. See #1044.
51
+ * Add ZUNION command. See #1042.
52
+ * Add HRANDFIELD command. See #1040.
53
+
54
+ # 4.5.1
55
+
56
+ * 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,
57
+ 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.
58
+ This behavior is deprecated and will be removed in Redis 4.6.0. Fix #1038.
59
+
60
+ # 4.5.0
61
+
62
+ * Handle parts of the command using incompatible encodings. See #1037.
63
+ * Add GET option to SET command. See #1036.
64
+ * Add ZRANDMEMBER command. See #1035.
65
+ * Add LMOVE/BLMOVE commands. See #1034.
66
+ * Add ZMSCORE command. See #1032.
67
+ * Add LT/GT options to ZADD. See #1033.
68
+ * Add SMISMEMBER command. See #1031.
69
+ * Add EXAT/PXAT options to SET. See #1028.
70
+ * Add GETDEL/GETEX commands. See #1024.
71
+ * `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`.
72
+ * Fix Redis < 6 detection during connect. See #1025.
73
+ * Fix fetching command details in Redis cluster when the first node is unhealthy. See #1026.
74
+
75
+ # 4.4.0
76
+
77
+ * Redis cluster: fix cross-slot validation in pipelines. Fix ##1019.
78
+ * Add support for `XAUTOCLAIM`. See #1018.
79
+ * Properly issue `READONLY` when reconnecting to replicas. Fix #1017.
80
+ * Make `del` a noop if passed an empty list of keys. See #998.
81
+ * Add support for `ZINTER`. See #995.
82
+
83
+ # 4.3.1
84
+
85
+ * Fix password authentication against redis server 5 and older.
86
+
87
+ # 4.3.0
88
+
89
+ * Add the TYPE argument to scan and scan_each. See #985.
90
+ * Support AUTH command for ACL. See #967.
91
+
92
+ # 4.2.5
93
+
94
+ * Optimize the ruby connector write buffering. See #964.
95
+
96
+ # 4.2.4
97
+
98
+ * Fix bytesize calculations in the ruby connector, and work on a copy of the buffer. Fix #961, #962.
99
+
100
+ # 4.2.3
101
+
102
+ * Use io/wait instead of IO.select in the ruby connector. See #960.
103
+ * Use exception free non blocking IOs in the ruby connector. See #926.
104
+ * Prevent corruption of the client when an interrupt happen during inside a pipeline block. See #945.
105
+
106
+ # 4.2.2
107
+
108
+ * Fix `WATCH` support for `Redis::Distributed`. See #941.
109
+ * Fix handling of empty stream responses. See #905, #929.
110
+
111
+ # 4.2.1
112
+
113
+ * Fix `exists?` returning an actual boolean when called with multiple keys. See #918.
114
+ * Setting `Redis.exists_returns_integer = false` disables warning message about new behaviour. See #920.
115
+
3
116
  # 4.2.0
4
117
 
5
118
  * Convert commands to accept keyword arguments rather than option hashes. This both help catching typos, and reduce needless allocations.
@@ -12,6 +125,7 @@
12
125
  * Optimized initialization of Redis::Cluster. See #912.
13
126
  * Accept sentinel options even with string key. See #599.
14
127
  * Verify TLS connections by default. See #900.
128
+ * Make `Redis#hset` variadic. It now returns an integer, not a boolean. See #910.
15
129
 
16
130
  # 4.1.4
17
131
 
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # redis-rb [![Build Status][travis-image]][travis-link] [![Inline docs][inchpages-image]][inchpages-link] ![](https://github.com/redis/redis-rb/workflows/Test/badge.svg?branch=master)
1
+ # redis-rb [![Build Status][gh-actions-image]][gh-actions-link] [![Inline docs][inchpages-image]][inchpages-link]
2
2
 
3
3
  A Ruby client that tries to match [Redis][redis-home]' API one-to-one, while still
4
4
  providing an idiomatic interface.
@@ -54,6 +54,12 @@ To connect to a password protected Redis instance, use:
54
54
  redis = Redis.new(password: "mysecret")
55
55
  ```
56
56
 
57
+ To connect a Redis instance using [ACL](https://redis.io/topics/acl), use:
58
+
59
+ ```ruby
60
+ redis = Redis.new(username: 'myname', password: 'mysecret')
61
+ ```
62
+
57
63
  The Redis class exports methods that are named identical to the commands
58
64
  they execute. The arguments these methods accept are often identical to
59
65
  the arguments specified on the [Redis website][redis-commands]. For
@@ -178,9 +184,9 @@ commands to Redis and gathers their replies. These replies are returned
178
184
  by the `#pipelined` method.
179
185
 
180
186
  ```ruby
181
- redis.pipelined do
182
- redis.set "foo", "bar"
183
- redis.incr "baz"
187
+ redis.pipelined do |pipeline|
188
+ pipeline.set "foo", "bar"
189
+ pipeline.incr "baz"
184
190
  end
185
191
  # => ["OK", 1]
186
192
  ```
@@ -194,9 +200,9 @@ the regular pipeline, the replies to the commands are returned by the
194
200
  `#multi` method.
195
201
 
196
202
  ```ruby
197
- redis.multi do
198
- redis.set "foo", "bar"
199
- redis.incr "baz"
203
+ redis.multi do |transaction|
204
+ transaction.set "foo", "bar"
205
+ transaction.incr "baz"
200
206
  end
201
207
  # => ["OK", 1]
202
208
  ```
@@ -204,15 +210,15 @@ end
204
210
  ### Futures
205
211
 
206
212
  Replies to commands in a pipeline can be accessed via the *futures* they
207
- emit (since redis-rb 3.0). All calls inside a pipeline block return a
213
+ emit (since redis-rb 3.0). All calls on the pipeline object return a
208
214
  `Future` object, which responds to the `#value` method. When the
209
215
  pipeline has successfully executed, all futures are assigned their
210
216
  respective replies and can be used.
211
217
 
212
218
  ```ruby
213
- redis.pipelined do
214
- @set = redis.set "foo", "bar"
215
- @incr = redis.incr "baz"
219
+ redis.pipelined do |pipeline|
220
+ @set = pipeline.set "foo", "bar"
221
+ @incr = pipeline.incr "baz"
216
222
  end
217
223
 
218
224
  @set.value
@@ -265,6 +271,7 @@ All timeout values are specified in seconds.
265
271
  When using pub/sub, you can subscribe to a channel using a timeout as well:
266
272
 
267
273
  ```ruby
274
+ redis = Redis.new(reconnect_attempts: 0)
268
275
  redis.subscribe_with_timeout(5, "news") do |on|
269
276
  on.message do |channel, message|
270
277
  # ...
@@ -439,7 +446,7 @@ redis = Redis.new(:driver => :synchrony)
439
446
  ## Testing
440
447
 
441
448
  This library is tested against recent Ruby and Redis versions.
442
- Check [Travis][travis-link] for the exact versions supported.
449
+ Check [Github Actions][gh-actions-link] for the exact versions supported.
443
450
 
444
451
  ## See Also
445
452
 
@@ -458,12 +465,11 @@ client and evangelized Redis in Rubyland. Thank you, Ezra.
458
465
  requests.
459
466
 
460
467
 
461
- [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
462
- [inchpages-link]: https://inch-ci.org/github/redis/redis-rb
463
- [redis-commands]: https://redis.io/commands
464
- [redis-home]: https://redis.io
465
- [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
466
- [travis-home]: https://travis-ci.org/
467
- [travis-image]: https://secure.travis-ci.org/redis/redis-rb.svg?branch=master
468
- [travis-link]: https://travis-ci.org/redis/redis-rb
469
- [rubydoc]: http://www.rubydoc.info/gems/redis
468
+ [inchpages-image]: https://inch-ci.org/github/redis/redis-rb.svg
469
+ [inchpages-link]: https://inch-ci.org/github/redis/redis-rb
470
+ [redis-commands]: https://redis.io/commands
471
+ [redis-home]: https://redis.io
472
+ [redis-url]: http://www.iana.org/assignments/uri-schemes/prov/redis
473
+ [gh-actions-image]: https://github.com/redis/redis-rb/workflows/Test/badge.svg
474
+ [gh-actions-link]: https://github.com/redis/redis-rb/actions
475
+ [rubydoc]: http://www.rubydoc.info/gems/redis
data/lib/redis/client.rb CHANGED
@@ -1,18 +1,23 @@
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
9
+ # Defaults are also used for converting string keys to symbols.
9
10
  DEFAULTS = {
10
11
  url: -> { ENV["REDIS_URL"] },
11
12
  scheme: "redis",
12
13
  host: "127.0.0.1",
13
14
  port: 6379,
14
15
  path: nil,
16
+ read_timeout: nil,
17
+ write_timeout: nil,
18
+ connect_timeout: nil,
15
19
  timeout: 5.0,
20
+ username: nil,
16
21
  password: nil,
17
22
  db: 0,
18
23
  driver: nil,
@@ -22,11 +27,12 @@ class Redis
22
27
  reconnect_delay: 0,
23
28
  reconnect_delay_max: 0.5,
24
29
  inherit_socket: false,
30
+ logger: nil,
25
31
  sentinels: nil,
26
32
  role: nil
27
33
  }.freeze
28
34
 
29
- attr_reader :options
35
+ attr_reader :options, :connection, :command_map
30
36
 
31
37
  def scheme
32
38
  @options[:scheme]
@@ -56,6 +62,10 @@ class Redis
56
62
  @options[:read_timeout]
57
63
  end
58
64
 
65
+ def username
66
+ @options[:username]
67
+ end
68
+
59
69
  def password
60
70
  @options[:password]
61
71
  end
@@ -77,8 +87,6 @@ class Redis
77
87
  end
78
88
 
79
89
  attr_accessor :logger
80
- attr_reader :connection
81
- attr_reader :command_map
82
90
 
83
91
  def initialize(options = {})
84
92
  @options = _parse_options(options)
@@ -105,7 +113,34 @@ class Redis
105
113
  # Don't try to reconnect when the connection is fresh
106
114
  with_reconnect(false) do
107
115
  establish_connection
108
- call [:auth, password] if password
116
+ if password
117
+ if username
118
+ begin
119
+ call [:auth, username, 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
137
+ end
138
+ else
139
+ call [:auth, password]
140
+ end
141
+ end
142
+
143
+ call [:readonly] if @options[:readonly]
109
144
  call [:select, db] if db != 0
110
145
  call [:client, :setname, @options[:id]] if @options[:id]
111
146
  @connector.check(self)
@@ -126,7 +161,7 @@ class Redis
126
161
  reply = process([command]) { read }
127
162
  raise reply if reply.is_a?(CommandError)
128
163
 
129
- if block_given?
164
+ if block_given? && reply != 'QUEUED'
130
165
  yield reply
131
166
  else
132
167
  reply
@@ -216,7 +251,8 @@ class Redis
216
251
  result
217
252
  end
218
253
 
219
- 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
220
256
  with_socket_timeout(timeout) do
221
257
  call(command, &blk)
222
258
  end
@@ -406,7 +442,7 @@ class Redis
406
442
  defaults = DEFAULTS.dup
407
443
  options = options.dup
408
444
 
409
- defaults.keys.each do |key|
445
+ defaults.each_key do |key|
410
446
  # Fill in defaults if needed
411
447
  defaults[key] = defaults[key].call if defaults[key].respond_to?(:call)
412
448
 
@@ -423,13 +459,15 @@ class Redis
423
459
 
424
460
  uri = URI(url)
425
461
 
426
- if uri.scheme == "unix"
462
+ case uri.scheme
463
+ when "unix"
427
464
  defaults[:path] = uri.path
428
- elsif uri.scheme == "redis" || uri.scheme == "rediss"
465
+ when "redis", "rediss"
429
466
  defaults[:scheme] = uri.scheme
430
467
  defaults[:host] = uri.host if uri.host
431
468
  defaults[:port] = uri.port if uri.port
432
- defaults[:password] = CGI.unescape(uri.password) if uri.password
469
+ defaults[:username] = CGI.unescape(uri.user) if uri.user && !uri.user.empty?
470
+ defaults[:password] = CGI.unescape(uri.password) if uri.password && !uri.password.empty?
433
471
  defaults[:db] = uri.path[1..-1].to_i if uri.path
434
472
  defaults[:role] = :master
435
473
  else
@@ -440,7 +478,7 @@ class Redis
440
478
  end
441
479
 
442
480
  # Use default when option is not specified or nil
443
- defaults.keys.each do |key|
481
+ defaults.each_key do |key|
444
482
  options[key] = defaults[key] if options[key].nil?
445
483
  end
446
484
 
@@ -505,7 +543,7 @@ class Redis
505
543
  require_relative "connection/#{driver}"
506
544
  rescue LoadError, NameError
507
545
  begin
508
- require "connection/#{driver}"
546
+ require "redis/connection/#{driver}"
509
547
  rescue LoadError, NameError => error
510
548
  raise "Cannot load driver #{driver.inspect}: #{error.message}"
511
549
  end
@@ -574,6 +612,7 @@ class Redis
574
612
  client = Client.new(@options.merge({
575
613
  host: sentinel[:host] || sentinel["host"],
576
614
  port: sentinel[:port] || sentinel["port"],
615
+ username: sentinel[:username] || sentinel["username"],
577
616
  password: sentinel[:password] || sentinel["password"],
578
617
  reconnect_attempts: 0
579
618
  }))
@@ -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
@@ -10,22 +10,21 @@ class Redis
10
10
  module_function
11
11
 
12
12
  def load(nodes)
13
- details = {}
14
-
15
13
  nodes.each do |node|
16
- details = fetch_command_details(node)
17
- details.empty? ? next : break
14
+ begin
15
+ return fetch_command_details(node)
16
+ rescue CannotConnectError, ConnectionError, CommandError
17
+ next # can retry on another node
18
+ end
18
19
  end
19
20
 
20
- details
21
+ raise CannotConnectError, 'Redis client could not connect to any cluster nodes'
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?
@@ -76,8 +88,9 @@ class Redis
76
88
  clients = options.map do |node_key, option|
77
89
  next if replica_disabled? && slave?(node_key)
78
90
 
91
+ option = option.merge(readonly: true) if slave?(node_key)
92
+
79
93
  client = Client.new(option)
80
- client.call(%i[readonly]) if slave?(node_key)
81
94
  [node_key, client]
82
95
  end
83
96
 
@@ -18,6 +18,7 @@ class Redis
18
18
  @node_opts = build_node_options(node_addrs)
19
19
  @replica = options.delete(:replica) == true
20
20
  add_common_node_option_if_needed(options, @node_opts, :scheme)
21
+ add_common_node_option_if_needed(options, @node_opts, :username)
21
22
  add_common_node_option_if_needed(options, @node_opts, :password)
22
23
  @options = options
23
24
  end
@@ -63,7 +64,9 @@ class Redis
63
64
  raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
64
65
 
65
66
  db = uri.path.split('/')[1]&.to_i
66
- { scheme: uri.scheme, password: uri.password, host: uri.host, port: uri.port, db: db }.reject { |_, v| v.nil? }
67
+
68
+ { scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
69
+ .reject { |_, v| v.nil? || v == '' }
67
70
  rescue URI::InvalidURIError => err
68
71
  raise InvalidClientOptionError, err.message
69
72
  end
@@ -79,7 +82,7 @@ class Redis
79
82
 
80
83
  # Redis cluster node returns only host and port information.
81
84
  # So we should complement additional information such as:
82
- # scheme, password and so on.
85
+ # scheme, username, password and so on.
83
86
  def add_common_node_option_if_needed(options, node_opts, key)
84
87
  return options if options[key].nil? && node_opts.first[key].nil?
85
88
 
data/lib/redis/cluster.rb CHANGED
@@ -78,11 +78,13 @@ class Redis
78
78
  end
79
79
 
80
80
  def call_pipeline(pipeline)
81
- node_keys, command_keys = extract_keys_in_pipeline(pipeline)
82
- raise CrossSlotPipeliningError, command_keys if node_keys.size > 1
81
+ node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq
82
+ if node_keys.size > 1
83
+ raise(CrossSlotPipeliningError,
84
+ pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
85
+ end
83
86
 
84
- node = find_node(node_keys.first)
85
- try_send(node, :call_pipeline, pipeline)
87
+ try_send(find_node(node_keys.first), :call_pipeline, pipeline)
86
88
  end
87
89
 
88
90
  def call_with_timeout(command, timeout, &block)
@@ -128,13 +130,14 @@ class Redis
128
130
  def send_command(command, &block)
129
131
  cmd = command.first.to_s.downcase
130
132
  case cmd
131
- when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
133
+ when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
132
134
  @node.call_all(command, &block).first
133
135
  when 'flushall', 'flushdb'
134
136
  @node.call_master(command, &block).first
135
137
  when 'wait' then @node.call_master(command, &block).reduce(:+)
136
138
  when 'keys' then @node.call_slave(command, &block).flatten.sort
137
139
  when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
140
+ when 'scan' then _scan(command, &block)
138
141
  when 'lastsave' then @node.call_all(command, &block).sort
139
142
  when 'role' then @node.call_all(command, &block)
140
143
  when 'config' then send_config_command(command, &block)
@@ -236,6 +239,29 @@ class Redis
236
239
  raise
237
240
  end
238
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
+
239
265
  def assign_redirection_node(err_msg)
240
266
  _, slot, node_key = err_msg.split(' ')
241
267
  slot = slot.to_i
@@ -253,14 +279,14 @@ class Redis
253
279
  find_node(node_key)
254
280
  end
255
281
 
256
- def find_node_key(command)
282
+ def find_node_key(command, primary_only: false)
257
283
  key = @command.extract_first_key(command)
258
284
  return if key.empty?
259
285
 
260
286
  slot = KeySlotConverter.convert(key)
261
287
  return unless @slot.exists?(slot)
262
288
 
263
- if @command.should_send_to_master?(command)
289
+ if @command.should_send_to_master?(command) || primary_only
264
290
  @slot.find_node_key_of_master(slot)
265
291
  else
266
292
  @slot.find_node_key_of_slave(slot)
@@ -285,11 +311,5 @@ class Redis
285
311
  @node.map(&:disconnect)
286
312
  @node, @slot = fetch_cluster_info!(@option)
287
313
  end
288
-
289
- def extract_keys_in_pipeline(pipeline)
290
- node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
291
- command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
292
- [node_keys, command_keys]
293
- end
294
314
  end
295
315
  end
@@ -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