Skip to content

Commit 4051d6a

Browse files
KJTsanaktsidisKJ Tsanaktsidis
authored andcommitted
Implement RedisClient::Cluster::Command#extract_all_keys
We need this in order to get the list of keys from a WATCH command. It's probably a little overkill, but may as well implemenet the whole thing.
1 parent 658103d commit 4051d6a

File tree

2 files changed

+88
-6
lines changed

2 files changed

+88
-6
lines changed

lib/redis_client/cluster/command.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,13 @@ class Command
1111
LEFT_BRACKET = '{'
1212
RIGHT_BRACKET = '}'
1313
EMPTY_HASH = {}.freeze
14+
EMPTY_ARRAY = [].freeze
1415

1516
Detail = Struct.new(
1617
'RedisCommand',
1718
:first_key_position,
19+
:last_key_position,
20+
:key_step,
1821
:write?,
1922
:readonly?,
2023
keyword_init: true
@@ -50,6 +53,8 @@ def parse_command_reply(rows)
5053

5154
acc[row[0].downcase] = ::RedisClient::Cluster::Command::Detail.new(
5255
first_key_position: row[3],
56+
last_key_position: row[4],
57+
key_step: row[5],
5358
write?: row[2].include?('write'),
5459
readonly?: row[2].include?('readonly')
5560
)
@@ -70,6 +75,17 @@ def extract_first_key(command)
7075
hash_tag.empty? ? key : hash_tag
7176
end
7277

78+
def extract_all_keys(command)
79+
keys_start = determine_first_key_position(command)
80+
keys_end = determine_last_key_position(command, keys_start)
81+
keys_step = determine_key_step(command)
82+
return EMPTY_ARRAY if [keys_start, keys_end, keys_step].any?(&:zero?)
83+
84+
keys_end = [keys_end, command.size - 1].min
85+
# use .. inclusive range because keys_end is a valid index.
86+
(keys_start..keys_end).step(keys_step).map { |i| command[i] }
87+
end
88+
7389
def should_send_to_primary?(command)
7490
name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
7591
@commands[name]&.write?
@@ -101,6 +117,41 @@ def determine_first_key_position(command) # rubocop:disable Metrics/CyclomaticCo
101117
end
102118
end
103119

120+
# IMPORTANT: this determines the last key position INCLUSIVE of the last key -
121+
# i.e. command[determine_last_key_position(command)] is a key.
122+
# This is in line with what Redis returns from COMMANDS.
123+
def determine_last_key_position(command, keys_start) # rubocop:disable Metrics/AbcSize
124+
case name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
125+
when 'eval', 'evalsha', 'zinterstore', 'zunionstore'
126+
# EVALSHA sha1 numkeys [key [key ...]] [arg [arg ...]]
127+
# ZINTERSTORE destination numkeys key [key ...] [WEIGHTS weight [weight ...]] [AGGREGATE <SUM | MIN | MAX>]
128+
command[2].to_i + 2
129+
when 'object', 'memory'
130+
# OBJECT [ENCODING | FREQ | IDLETIME | REFCOUNT] key
131+
# MEMORY USAGE key [SAMPLES count]
132+
keys_start
133+
when 'migrate'
134+
# MIGRATE host port <key | ""> destination-db timeout [COPY] [REPLACE] [AUTH password | AUTH2 username password] [KEYS key [key ...]]
135+
command[3].empty? ? (command.length - 1) : 3
136+
when 'xread', 'xreadgroup'
137+
# XREAD [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] id [id ...]
138+
keys_start + ((command.length - keys_start) / 2) - 1
139+
else
140+
# If there is a fixed, non-variable number of keys, don't iterate past that.
141+
if @commands[name].last_key_position >= 0
142+
@commands[name].last_key_position
143+
else
144+
command.length + @commands[name].last_key_position
145+
end
146+
end
147+
end
148+
149+
def determine_key_step(command)
150+
name = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_command(command)
151+
# Some commands like EVALSHA have zero as the step in COMMANDS somehow.
152+
@commands[name].key_step == 0 ? 1 : @commands[name].key_step
153+
end
154+
104155
def determine_optional_key_position(command, option_name) # rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
105156
idx = command&.flatten&.map(&:to_s)&.map(&:downcase)&.index(option_name&.downcase)
106157
idx.nil? ? 0 : idx + 1

test/redis_client/cluster/test_command.rb

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,20 +49,20 @@ def test_parse_command_reply
4949
[
5050
{
5151
rows: [
52-
['get', 2, Set['readonly', 'fast'], 1, 1, 1, Set['@read', '@string', '@fast'], Set[], Set[], Set[]],
53-
['set', -3, Set['write', 'denyoom', 'movablekeys'], 1, 1, 1, Set['@write', '@string', '@slow'], Set[], Set[], Set[]]
52+
['get', 2, Set['readonly', 'fast'], 1, -1, 1, Set['@read', '@string', '@fast'], Set[], Set[], Set[]],
53+
['set', -3, Set['write', 'denyoom', 'movablekeys'], 1, -1, 2, Set['@write', '@string', '@slow'], Set[], Set[], Set[]]
5454
],
5555
want: {
56-
'get' => { first_key_position: 1, write?: false, readonly?: true },
57-
'set' => { first_key_position: 1, write?: true, readonly?: false }
56+
'get' => { first_key_position: 1, last_key_position: -1, key_step: 1, write?: false, readonly?: true },
57+
'set' => { first_key_position: 1, last_key_position: -1, key_step: 2, write?: true, readonly?: false }
5858
}
5959
},
6060
{
6161
rows: [
62-
['GET', 2, Set['readonly', 'fast'], 1, 1, 1, Set['@read', '@string', '@fast'], Set[], Set[], Set[]]
62+
['GET', 2, Set['readonly', 'fast'], 1, -1, 1, Set['@read', '@string', '@fast'], Set[], Set[], Set[]]
6363
],
6464
want: {
65-
'get' => { first_key_position: 1, write?: false, readonly?: true }
65+
'get' => { first_key_position: 1, last_key_position: -1, key_step: 1, write?: false, readonly?: true }
6666
}
6767
},
6868
{ rows: [[]], want: {} },
@@ -212,6 +212,37 @@ def test_extract_hash_tag
212212
assert_equal(c[:want], got, msg)
213213
end
214214
end
215+
216+
def test_extract_all_keys
217+
cmd = ::RedisClient::Cluster::Command.load(@raw_clients)
218+
[
219+
{ command: ['EVAL', 'return ARGV[1]', '0', 'hello'], want: [] },
220+
{ command: ['EVAL', 'return ARGV[1]', '3', 'key1', 'key2', 'key3', 'arg1', 'arg2'], want: %w[key1 key2 key3] },
221+
{ command: [['EVAL'], '"return ARGV[1]"', 0, 'hello'], want: [] },
222+
{ command: %w[EVALSHA sha1 2 foo bar baz zap], want: %w[foo bar] },
223+
{ command: %w[MIGRATE host port key 0 5 COPY], want: %w[key] },
224+
{ command: ['MIGRATE', 'host', 'port', '', '0', '5', 'COPY', 'KEYS', 'key1'], want: %w[key1] },
225+
{ command: ['MIGRATE', 'host', 'port', '', '0', '5', 'COPY', 'KEYS', 'key1', 'key2'], want: %w[key1 key2] },
226+
{ command: %w[ZINTERSTORE out 2 zset1 zset2 WEIGHTS 2 3], want: %w[zset1 zset2] },
227+
{ command: %w[ZUNIONSTORE out 2 zset1 zset2 WEIGHTS 2 3], want: %w[zset1 zset2] },
228+
{ command: %w[OBJECT HELP], want: [] },
229+
{ command: %w[MEMORY HELP], want: [] },
230+
{ command: %w[MEMORY USAGE key], want: %w[key] },
231+
{ command: %w[XREAD COUNT 2 STREAMS mystream writers 0-0 0-0], want: %w[mystream writers] },
232+
{ command: %w[XREADGROUP GROUP group consumer STREAMS key id], want: %w[key] },
233+
{ command: %w[SET foo 1], want: %w[foo] },
234+
{ command: %w[set foo 1], want: %w[foo] },
235+
{ command: [['SET'], 'foo', 1], want: %w[foo] },
236+
{ command: %w[GET foo], want: %w[foo] },
237+
{ command: %w[MGET foo bar baz], want: %w[foo bar baz] },
238+
{ command: %w[MSET foo val bar val baz val], want: %w[foo bar baz] },
239+
{ command: %w[BLPOP foo bar 0], want: %w[foo bar] }
240+
].each_with_index do |c, idx|
241+
msg = "Case: #{idx}"
242+
got = cmd.send(:extract_all_keys, c[:command])
243+
assert_equal(c[:want], got, msg)
244+
end
245+
end
215246
end
216247
end
217248
end

0 commit comments

Comments
 (0)