Skip to content

Commit b639c43

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 d82a13e commit b639c43

File tree

5 files changed

+55
-49
lines changed

5 files changed

+55
-49
lines changed

lib/redis_client/cluster/command.rb

Lines changed: 2 additions & 17 deletions
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
EMPTY_ARRAY = [].freeze
1514

@@ -70,9 +69,7 @@ def extract_first_key(command)
7069
i = determine_first_key_position(command)
7170
return EMPTY_STRING if i == 0
7271

73-
key = (command[i].is_a?(Array) ? command[i].flatten.first : command[i]).to_s
74-
hash_tag = extract_hash_tag(key)
75-
hash_tag.empty? ? key : hash_tag
72+
(command[i].is_a?(Array) ? command[i].flatten.first : command[i]).to_s
7673
end
7774

7875
def extract_all_keys(command)
@@ -156,18 +153,6 @@ def determine_optional_key_position(command, option_name) # rubocop:disable Metr
156153
idx = command&.flatten&.map(&:to_s)&.map(&:downcase)&.index(option_name&.downcase)
157154
idx.nil? ? 0 : idx + 1
158155
end
159-
160-
# @see https://redis.io/topics/cluster-spec#keys-hash-tags Keys hash tags
161-
def extract_hash_tag(key)
162-
key = key.to_s
163-
s = key.index(LEFT_BRACKET)
164-
return EMPTY_STRING if s.nil?
165-
166-
e = key.index(RIGHT_BRACKET, s + 1)
167-
return EMPTY_STRING if e.nil?
168-
169-
key[s + 1..e - 1]
170-
end
171156
end
172157
end
173158
end

lib/redis_client/cluster/key_slot_converter.rb

Lines changed: 18 additions & 0 deletions
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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -167,21 +167,25 @@ def assign_node(command)
167167
find_node(node_key)
168168
end
169169

170-
def find_node_key(command, seed: nil)
171-
key = @command.extract_first_key(command)
172-
slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
173-
174-
if @command.should_send_to_primary?(command)
175-
@node.find_node_key_of_primary(slot) || @node.any_primary_node_key(seed: seed)
170+
def find_node_key_by_key(key, seed: nil, primary: false)
171+
if key && !key.empty?
172+
slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
173+
primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
176174
else
177-
@node.find_node_key_of_replica(slot, seed: seed) || @node.any_replica_node_key(seed: seed)
175+
primary ? @node.any_primary_node_key(seed: seed) : @node.any_replica_node_key(seed: seed)
178176
end
179177
end
180178

179+
def find_node_key(command, seed: nil)
180+
key = @command.extract_first_key(command)
181+
find_node_key_by_key(key, seed: seed, primary: @command.should_send_to_primary?(command))
182+
end
183+
181184
def find_primary_node_key(command)
182185
key = @command.extract_first_key(command)
183-
slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
184-
@node.find_node_key_of_primary(slot)
186+
return nil unless key&.size&.> 0
187+
188+
find_node_key_by_key(key, primary: true)
185189
end
186190

187191
def find_node(node_key, retry_count: 3)

test/redis_client/cluster/test_command.rb

Lines changed: 1 addition & 23 deletions
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' },
@@ -191,28 +191,6 @@ def test_determine_optional_key_position
191191
end
192192
end
193193

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
215-
216194
def test_extract_all_keys
217195
cmd = ::RedisClient::Cluster::Command.load(@raw_clients)
218196
[

test/redis_client/cluster/test_key_slot_converter.rb

Lines changed: 21 additions & 0 deletions
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)