Skip to content

Commit 31968f3

Browse files
author
KJ Tsanaktsidis
committed
Refactor key-slot conversion logic
Previously, extract_first_key would perform hashtag extraction on the key it pulled from command; that meant that the "key" it was returning might not actually be the key in the command. This commit refactors things so that * extract_first_key actually extracts the first key * KeySlotConverter.convert does hashtag extraction * Router's find_node_key and find_primary_node_key can now be implemented in terms of a function "find_node_by_key", because they _actually_ get the key from the command.
1 parent f135ec0 commit 31968f3

File tree

5 files changed

+55
-49
lines changed

5 files changed

+55
-49
lines changed

lib/redis_client/cluster/command.rb

+2-17
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,13 @@
22

33
require 'redis_client'
44
require 'redis_client/cluster/errors'
5+
require 'redis_client/cluster/key_slot_converter'
56
require 'redis_client/cluster/normalized_cmd_name'
67

78
class RedisClient
89
class Cluster
910
class Command
1011
EMPTY_STRING = ''
11-
LEFT_BRACKET = '{'
12-
RIGHT_BRACKET = '}'
1312
EMPTY_HASH = {}.freeze
1413

1514
Detail = Struct.new(
@@ -65,9 +64,7 @@ def extract_first_key(command)
6564
i = determine_first_key_position(command)
6665
return EMPTY_STRING if i == 0
6766

68-
key = (command[i].is_a?(Array) ? command[i].flatten.first : command[i]).to_s
69-
hash_tag = extract_hash_tag(key)
70-
hash_tag.empty? ? key : hash_tag
67+
(command[i].is_a?(Array) ? command[i].flatten.first : command[i]).to_s
7168
end
7269

7370
def should_send_to_primary?(command)
@@ -105,18 +102,6 @@ def determine_optional_key_position(command, option_name) # rubocop:disable Metr
105102
idx = command&.flatten&.map(&:to_s)&.map(&:downcase)&.index(option_name&.downcase)
106103
idx.nil? ? 0 : idx + 1
107104
end
108-
109-
# @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
110-
def extract_hash_tag(key)
111-
key = key.to_s
112-
s = key.index(LEFT_BRACKET)
113-
return EMPTY_STRING if s.nil?
114-
115-
e = key.index(RIGHT_BRACKET, s + 1)
116-
return EMPTY_STRING if e.nil?
117-
118-
key[s + 1..e - 1]
119-
end
120105
end
121106
end
122107
end

lib/redis_client/cluster/key_slot_converter.rb

+18
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
class RedisClient
44
class Cluster
55
module KeySlotConverter
6+
EMPTY_STRING = ''
7+
LEFT_BRACKET = '{'
8+
RIGHT_BRACKET = '}'
69
XMODEM_CRC16_LOOKUP = [
710
0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7,
811
0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef,
@@ -45,13 +48,28 @@ module KeySlotConverter
4548
def convert(key)
4649
return nil if key.nil?
4750

51+
hash_tag = extract_hash_tag(key)
52+
key = hash_tag unless hash_tag.empty?
53+
4854
crc = 0
4955
key.each_byte do |b|
5056
crc = ((crc << 8) & 0xffff) ^ XMODEM_CRC16_LOOKUP[((crc >> 8) ^ b) & 0xff]
5157
end
5258

5359
crc % HASH_SLOTS
5460
end
61+
62+
# @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
63+
def extract_hash_tag(key)
64+
key = key.to_s
65+
s = key.index(LEFT_BRACKET)
66+
return EMPTY_STRING if s.nil?
67+
68+
e = key.index(RIGHT_BRACKET, s + 1)
69+
return EMPTY_STRING if e.nil?
70+
71+
key[s + 1..e - 1]
72+
end
5573
end
5674
end
5775
end

lib/redis_client/cluster/router.rb

+13-9
Original file line numberDiff line numberDiff line change
@@ -172,21 +172,25 @@ def assign_node(command)
172172
find_node(node_key)
173173
end
174174

175-
def find_node_key(command, seed: nil)
176-
key = @command.extract_first_key(command)
177-
slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
178-
179-
if @command.should_send_to_primary?(command)
180-
@node.find_node_key_of_primary(slot) || @node.any_primary_node_key(seed: seed)
175+
def find_node_key_by_key(key, seed: nil, primary: false)
176+
if key && !key.empty?
177+
slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
178+
primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
181179
else
182-
@node.find_node_key_of_replica(slot, seed: seed) || @node.any_replica_node_key(seed: seed)
180+
primary ? @node.any_primary_node_key(seed: seed) : @node.any_replica_node_key(seed: seed)
183181
end
184182
end
185183

184+
def find_node_key(command, seed: nil)
185+
key = @command.extract_first_key(command)
186+
find_node_key_by_key(key, seed: seed, primary: @command.should_send_to_primary?(command))
187+
end
188+
186189
def find_primary_node_key(command)
187190
key = @command.extract_first_key(command)
188-
slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
189-
@node.find_node_key_of_primary(slot)
191+
return nil unless key&.size&.> 0
192+
193+
find_node_key_by_key(key, primary: true)
190194
end
191195

192196
def find_node(node_key, retry_count: 3)

test/redis_client/cluster/test_command.rb

+1-23
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def test_extract_first_key
8484
[
8585
{ command: %w[SET foo 1], want: 'foo' },
8686
{ command: %w[GET foo], want: 'foo' },
87-
{ command: %w[GET foo{bar}baz], want: 'bar' },
87+
{ command: %w[GET foo{bar}baz], want: 'foo{bar}baz' },
8888
{ command: %w[MGET foo bar baz], want: 'foo' },
8989
{ command: %w[UNKNOWN foo bar], want: '' },
9090
{ command: [['GET'], 'foo'], want: 'foo' },
@@ -190,28 +190,6 @@ def test_determine_optional_key_position
190190
assert_equal(c[:want], got, msg)
191191
end
192192
end
193-
194-
def test_extract_hash_tag
195-
cmd = ::RedisClient::Cluster::Command.load(@raw_clients)
196-
[
197-
{ key: 'foo', want: '' },
198-
{ key: 'foo{bar}baz', want: 'bar' },
199-
{ key: 'foo{bar}baz{qux}quuc', want: 'bar' },
200-
{ key: 'foo}bar{baz', want: '' },
201-
{ key: 'foo{bar', want: '' },
202-
{ key: 'foo}bar', want: '' },
203-
{ key: 'foo{}bar', want: '' },
204-
{ key: '{}foo', want: '' },
205-
{ key: 'foo{}', want: '' },
206-
{ key: '{}', want: '' },
207-
{ key: '', want: '' },
208-
{ key: nil, want: '' }
209-
].each_with_index do |c, idx|
210-
msg = "Case: #{idx}"
211-
got = cmd.send(:extract_hash_tag, c[:key])
212-
assert_equal(c[:want], got, msg)
213-
end
214-
end
215193
end
216194
end
217195
end

test/redis_client/cluster/test_key_slot_converter.rb

+21
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,27 @@ def test_convert
2727
got = ::RedisClient::Cluster::KeySlotConverter.convert(multi_byte_key)
2828
assert_equal(want, got, "Case: #{multi_byte_key}")
2929
end
30+
31+
def test_extract_hash_tag
32+
[
33+
{ key: 'foo', want: '' },
34+
{ key: 'foo{bar}baz', want: 'bar' },
35+
{ key: 'foo{bar}baz{qux}quuc', want: 'bar' },
36+
{ key: 'foo}bar{baz', want: '' },
37+
{ key: 'foo{bar', want: '' },
38+
{ key: 'foo}bar', want: '' },
39+
{ key: 'foo{}bar', want: '' },
40+
{ key: '{}foo', want: '' },
41+
{ key: 'foo{}', want: '' },
42+
{ key: '{}', want: '' },
43+
{ key: '', want: '' },
44+
{ key: nil, want: '' }
45+
].each_with_index do |c, idx|
46+
msg = "Case: #{idx}"
47+
got = ::RedisClient::Cluster::KeySlotConverter.extract_hash_tag(c[:key])
48+
assert_equal(c[:want], got, msg)
49+
end
50+
end
3051
end
3152
end
3253
end

0 commit comments

Comments
 (0)