Skip to content

Commit 966ae6c

Browse files
authored
fix: use the slot for the validation in the transaction (#330)
1 parent 9944228 commit 966ae6c

File tree

4 files changed

+32
-42
lines changed

4 files changed

+32
-42
lines changed

lib/redis_client/cluster.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,9 @@ def multi(watch: nil)
9898
return transaction.execute
9999
end
100100

101-
::RedisClient::Cluster::OptimisticLocking.new(@router).watch(watch) do |c, resharding|
101+
::RedisClient::Cluster::OptimisticLocking.new(@router).watch(watch) do |c, slot|
102102
transaction = ::RedisClient::Cluster::Transaction.new(
103-
@router, @command_builder, node: c, resharding: resharding
103+
@router, @command_builder, node: c, slot: slot
104104
)
105105
yield transaction
106106
transaction.execute

lib/redis_client/cluster/optimistic_locking.rb

+10-27
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
# frozen_string_literal: true
22

33
require 'redis_client'
4-
require 'redis_client/cluster/key_slot_converter'
54
require 'redis_client/cluster/transaction'
65

76
class RedisClient
@@ -12,15 +11,14 @@ def initialize(router)
1211
end
1312

1413
def watch(keys)
15-
ensure_safe_keys(keys)
16-
node = find_node(keys)
17-
cnt = 0 # We assume redirects occurred when incrementing it.
14+
slot = find_slot(keys)
15+
raise ::RedisClient::Cluster::Transaction::ConsistencyError, "unsafe watch: #{keys.join(' ')}" if slot.nil?
1816

17+
node = @router.find_primary_node_by_slot(slot)
1918
@router.handle_redirection(node, retry_count: 1) do |nd|
20-
cnt += 1
2119
nd.with do |c|
2220
c.call('WATCH', *keys)
23-
reply = yield(c, cnt > 1)
21+
reply = yield(c, slot)
2422
c.call('UNWATCH')
2523
reply
2624
end
@@ -29,29 +27,14 @@ def watch(keys)
2927

3028
private
3129

32-
def ensure_safe_keys(keys)
33-
return if safe?(keys)
30+
def find_slot(keys)
31+
return if keys.empty?
32+
return if keys.any? { |k| k.nil? || k.empty? }
3433

35-
raise ::RedisClient::Cluster::Transaction::ConsistencyError, "unsafe watch: #{keys.join(' ')}"
36-
end
37-
38-
def safe?(keys)
39-
return false if keys.empty?
40-
41-
slots = keys.map do |k|
42-
return false if k.nil? || k.empty?
43-
44-
::RedisClient::Cluster::KeySlotConverter.convert(k)
45-
end
46-
47-
slots.uniq.size == 1
48-
end
49-
50-
def find_node(keys)
51-
node_key = @router.find_primary_node_key(['WATCH', *keys])
52-
return @router.find_node(node_key) unless node_key.nil?
34+
slots = keys.map { |k| @router.find_slot_by_key(k) }
35+
return if slots.uniq.size != 1
5336

54-
raise ::RedisClient::Cluster::Transaction::ConsistencyError, "couldn't determine the node"
37+
slots.first
5538
end
5639
end
5740
end

lib/redis_client/cluster/router.rb

+10
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,16 @@ def find_primary_node_key(command)
179179
find_node_key_by_key(key, primary: true)
180180
end
181181

182+
def find_slot(command)
183+
find_slot_by_key(@command.extract_first_key(command))
184+
end
185+
186+
def find_slot_by_key(key)
187+
return if key.empty?
188+
189+
::RedisClient::Cluster::KeySlotConverter.convert(key)
190+
end
191+
182192
def find_node(node_key, retry_count: 3)
183193
@node.find_by(node_key)
184194
rescue ::RedisClient::Cluster::Node::ReloadNeeded

lib/redis_client/cluster/transaction.rb

+10-13
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,22 @@
22

33
require 'redis_client'
44
require 'redis_client/cluster/pipeline'
5-
require 'redis_client/cluster/node_key'
65

76
class RedisClient
87
class Cluster
98
class Transaction
109
ConsistencyError = Class.new(::RedisClient::Error)
1110
MAX_REDIRECTION = 2
1211

13-
def initialize(router, command_builder, node: nil, resharding: false)
12+
def initialize(router, command_builder, node: nil, slot: nil)
1413
@router = router
1514
@command_builder = command_builder
1615
@retryable = true
1716
@pipeline = ::RedisClient::Pipeline.new(@command_builder)
1817
@pending_commands = []
1918
@node = node
2019
prepare_tx unless @node.nil?
21-
@resharding_state = resharding
20+
@watching_slot = slot
2221
end
2322

2423
def call(*command, **kwargs, &block)
@@ -111,7 +110,7 @@ def send_pipeline(client, redirect:)
111110
client.middlewares.call_pipelined(commands, client.config) do
112111
connection.call_pipelined(commands, nil)
113112
rescue ::RedisClient::CommandError => e
114-
return handle_command_error!(client, commands, e, redirect: redirect) unless redirect.zero?
113+
return handle_command_error!(commands, e, redirect: redirect) unless redirect.zero?
115114

116115
raise
117116
end
@@ -140,28 +139,26 @@ def coerce_results!(results, offset: 1)
140139
results
141140
end
142141

143-
def handle_command_error!(client, commands, err, redirect:) # rubocop:disable Metrics/AbcSize
142+
def handle_command_error!(commands, err, redirect:) # rubocop:disable Metrics/AbcSize
144143
if err.message.start_with?('CROSSSLOT')
145144
raise ConsistencyError, "#{err.message}: #{err.command}"
146145
elsif err.message.start_with?('MOVED')
147-
ensure_the_same_node!(client, commands)
146+
ensure_the_same_slot!(commands)
148147
node = @router.assign_redirection_node(err.message)
149148
send_transaction(node, redirect: redirect - 1)
150149
elsif err.message.start_with?('ASK')
151-
ensure_the_same_node!(client, commands)
150+
ensure_the_same_slot!(commands)
152151
node = @router.assign_asking_node(err.message)
153152
try_asking(node) ? send_transaction(node, redirect: redirect - 1) : err
154153
else
155154
raise err
156155
end
157156
end
158157

159-
def ensure_the_same_node!(client, commands)
160-
node_keys = commands.map { |command| @router.find_primary_node_key(command) }.compact.uniq
161-
expected_node_key = ::RedisClient::Cluster::NodeKey.build_from_client(client)
162-
163-
return if !@resharding_state && node_keys.size == 1 && node_keys.first == expected_node_key
164-
return if @resharding_state && node_keys.size == 1
158+
def ensure_the_same_slot!(commands)
159+
slots = commands.map { |command| @router.find_slot(command) }.compact.uniq
160+
return if slots.size == 1 && @watching_slot.nil?
161+
return if slots.size == 1 && @watching_slot == slots.first
165162

166163
raise(ConsistencyError, "the transaction should be executed to a slot in a node: #{commands}")
167164
end

0 commit comments

Comments
 (0)