Skip to content

Commit 2d324a6

Browse files
authored
feat: support transaction (#277)
1 parent 8c18ce3 commit 2d324a6

File tree

5 files changed

+126
-0
lines changed

5 files changed

+126
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ The following methods are able to be used like `redis-client`.
113113
* `#hscan`
114114
* `#zscan`
115115
* `#pipelined`
116+
* `#multi`
116117
* `#pubsub`
117118
* `#close`
118119

lib/redis_client/cluster.rb

+8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
require 'redis_client/cluster/pipeline'
55
require 'redis_client/cluster/pub_sub'
66
require 'redis_client/cluster/router'
7+
require 'redis_client/cluster/transaction'
78

89
class RedisClient
910
class Cluster
@@ -88,6 +89,13 @@ def pipelined
8889
pipeline.execute
8990
end
9091

92+
def multi(watch: nil, &block)
93+
::RedisClient::Cluster::Transaction
94+
.new(@router, @command_builder)
95+
.find_node(&block)
96+
.multi(watch: watch, &block)
97+
end
98+
9199
def pubsub
92100
::RedisClient::Cluster::PubSub.new(@router, @command_builder)
93101
end

lib/redis_client/cluster/router.rb

+6
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,12 @@ def find_node_key(command, seed: nil)
179179
end
180180
end
181181

182+
def find_primary_node_key(command)
183+
key = @command.extract_first_key(command)
184+
slot = key.empty? ? nil : ::RedisClient::Cluster::KeySlotConverter.convert(key)
185+
@node.find_node_key_of_primary(slot)
186+
end
187+
182188
def find_node(node_key, retry_count: 3)
183189
@node.find_by(node_key)
184190
rescue ::RedisClient::Cluster::Node::ReloadNeeded
+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
require 'redis_client'
4+
5+
class RedisClient
6+
class Cluster
7+
class Transaction
8+
ConsistencyError = Class.new(::RedisClient::Error)
9+
10+
def initialize(router, command_builder)
11+
@router = router
12+
@command_builder = command_builder
13+
@node_key = nil
14+
end
15+
16+
def call(*command, **kwargs, &_)
17+
command = @command_builder.generate(command, kwargs)
18+
ensure_node_key(command)
19+
end
20+
21+
def call_v(command, &_)
22+
command = @command_builder.generate(command)
23+
ensure_node_key(command)
24+
end
25+
26+
def call_once(*command, **kwargs, &_)
27+
command = @command_builder.generate(command, kwargs)
28+
ensure_node_key(command)
29+
end
30+
31+
def call_once_v(command, &_)
32+
command = @command_builder.generate(command)
33+
ensure_node_key(command)
34+
end
35+
36+
def find_node
37+
yield self
38+
raise ArgumentError, 'empty transaction' if @node_key.nil?
39+
40+
@router.find_node(@node_key)
41+
end
42+
43+
private
44+
45+
def ensure_node_key(command)
46+
node_key = @router.find_primary_node_key(command)
47+
raise ConsistencyError, "Client couldn't determine the node to be executed the transaction by: #{command}" if node_key.nil?
48+
49+
@node_key ||= node_key
50+
raise ConsistencyError, "The transaction should be done for single node: #{@node_key}, #{node_key}" if node_key != @node_key
51+
52+
nil
53+
end
54+
end
55+
end
56+
end

test/redis_client/test_cluster.rb

+55
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,61 @@ def test_pubsub_without_subscription
187187
pubsub.close
188188
end
189189

190+
def test_transaction_with_single_key
191+
want = ['OK', 1, 2, '2']
192+
got = @client.multi do |t|
193+
t.call('SET', 'counter', '0')
194+
t.call('INCR', 'counter')
195+
t.call('INCR', 'counter')
196+
t.call('GET', 'counter')
197+
end
198+
199+
assert_equal(want, got)
200+
end
201+
202+
def test_transaction_with_multiple_key
203+
assert_raises(::RedisClient::Cluster::Transaction::ConsistencyError) do
204+
@client.multi do |t|
205+
t.call('SET', 'key1', '1')
206+
t.call('SET', 'key2', '2')
207+
t.call('SET', 'key3', '3')
208+
end
209+
end
210+
211+
(1..3).each do |i|
212+
assert_nil(@client.call('GET', "key#{i}"))
213+
end
214+
end
215+
216+
def test_transaction_with_empty_block
217+
assert_raises(ArgumentError) { @client.multi {} }
218+
end
219+
220+
def test_transaction_with_hashtag
221+
want = ['OK', 'OK', %w[1 2 3 4]]
222+
got = @client.multi do |t|
223+
t.call('MSET', '{key}1', '1', '{key}2', '2')
224+
t.call('MSET', '{key}3', '3', '{key}4', '4')
225+
t.call('MGET', '{key}1', '{key}2', '{key}3', '{key}4')
226+
end
227+
228+
assert_equal(want, got)
229+
end
230+
231+
def test_transaction_without_hashtag
232+
assert_raises(::RedisClient::Cluster::Transaction::ConsistencyError) do
233+
@client.multi do |t|
234+
t.call('MSET', 'key1', '1', 'key2', '2')
235+
t.call('MSET', 'key3', '3', 'key4', '4')
236+
t.call('MGET', 'key1', 'key2', 'key3', 'key4')
237+
end
238+
end
239+
240+
(1..4).each do |i|
241+
assert_nil(@client.call('GET', "key#{i}"))
242+
end
243+
end
244+
190245
def test_pubsub_with_wrong_command
191246
pubsub = @client.pubsub
192247
assert_nil(pubsub.call('SUBWAY'))

0 commit comments

Comments
 (0)