Skip to content

Commit ecca727

Browse files
committed
Keep compatibilities as possible for the transaction feature between a standalone client and a cluster client
1 parent cdfe172 commit ecca727

File tree

6 files changed

+149
-24
lines changed

6 files changed

+149
-24
lines changed

cluster/bin/console

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env ruby
2+
# frozen_string_literal: true
3+
4+
require 'irb'
5+
require 'bundler/setup'
6+
require 'redis/cluster'
7+
8+
IRB.start(File.expand_path('..', __dir__))

cluster/lib/redis/cluster.rb

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,20 @@ def cluster(subcommand, *args)
9696
send_command([:cluster, subcommand] + args, &block)
9797
end
9898

99+
# @example A typical use case.
100+
# redis.watch("key") do |client| # The client is an instance of the adapter
101+
# if redis.get("key") == "some value" # We can't use the client passed by the block argument
102+
# client.multi do |tx| # The tx is the same instance of the adapter
103+
# tx.set("key", "other value")
104+
# tx.incr("counter")
105+
# end
106+
# else
107+
# client.unwatch
108+
# end
109+
# end
110+
# # => ["OK", 6]
99111
def watch(*keys, &block)
100-
synchronize { |c| c.call_v([:watch] + keys, &block) }
112+
synchronize { |c| c.watch(*keys, &block) }
101113
end
102114

103115
private

cluster/lib/redis/cluster/client.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require 'redis-cluster-client'
4+
require 'redis/cluster/transaction_adapter'
45

56
class Redis
67
class Cluster
@@ -98,6 +99,22 @@ def multi(watch: nil, &block)
9899
handle_errors { super(watch: watch, &block) }
99100
end
100101

102+
def watch(*keys)
103+
unless block_given?
104+
raise Redis::Cluster::TransactionConsistencyError, 'A block is required if you use the cluster client.'
105+
end
106+
107+
handle_errors do
108+
RedisClient::Cluster::OptimisticLocking.new(@router).watch(keys) do |c, slot, asking|
109+
transaction = Redis::Cluster::TransactionAdapter.new(
110+
self, @router, @command_builder, node: c, slot: slot, asking: asking
111+
)
112+
yield transaction
113+
transaction.execute
114+
end
115+
end
116+
end
117+
101118
private
102119

103120
def handle_errors
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# frozen_string_literal: true
2+
3+
require 'redis_client/cluster/transaction'
4+
5+
class Redis
6+
class Cluster
7+
class TransactionAdapter < RedisClient::Cluster::Transaction
8+
def initialize(client, router, command_builder, node: nil, slot: nil, asking: false)
9+
@client = client
10+
super(router, command_builder, node: node, slot: slot, asking: asking)
11+
end
12+
13+
def multi
14+
yield self
15+
end
16+
17+
def exec
18+
# no need to do nothing
19+
end
20+
21+
def discard
22+
# no need to do nothing
23+
end
24+
25+
def watch(*_)
26+
# no need to do nothing
27+
end
28+
29+
def unwatch
30+
# no need to do nothing
31+
end
32+
33+
private
34+
35+
def method_missing(name, *args, **kwargs, &block)
36+
return call(name, *args, **kwargs, &block) if @client.respond_to?(name)
37+
38+
super
39+
end
40+
41+
def respond_to_missing?(name, include_private = false)
42+
return true if @client.respond_to?(name)
43+
44+
super
45+
end
46+
end
47+
end
48+
end

cluster/test/client_transactions_test.rb

Lines changed: 57 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@ class TestClusterClientTransactions < Minitest::Test
77
include Helper::Cluster
88

99
def test_cluster_client_does_support_transaction_by_single_key
10-
actual = redis.multi do |r|
11-
r.set('counter', '0')
12-
r.incr('counter')
13-
r.incr('counter')
10+
actual = redis.multi do |tx|
11+
tx.set('counter', '0')
12+
tx.incr('counter')
13+
tx.incr('counter')
1414
end
1515

1616
assert_equal(['OK', 1, 2], actual)
1717
assert_equal('2', redis.get('counter'))
1818
end
1919

2020
def test_cluster_client_does_support_transaction_by_hashtag
21-
actual = redis.multi do |r|
22-
r.mset('{key}1', 1, '{key}2', 2)
23-
r.mset('{key}3', 3, '{key}4', 4)
21+
actual = redis.multi do |tx|
22+
tx.mset('{key}1', 1, '{key}2', 2)
23+
tx.mset('{key}3', 3, '{key}4', 4)
2424
end
2525

2626
assert_equal(%w[OK OK], actual)
@@ -29,18 +29,18 @@ def test_cluster_client_does_support_transaction_by_hashtag
2929

3030
def test_cluster_client_does_not_support_transaction_by_multiple_keys
3131
assert_raises(Redis::Cluster::TransactionConsistencyError) do
32-
redis.multi do |r|
33-
r.set('key1', 1)
34-
r.set('key2', 2)
35-
r.set('key3', 3)
36-
r.set('key4', 4)
32+
redis.multi do |tx|
33+
tx.set('key1', 1)
34+
tx.set('key2', 2)
35+
tx.set('key3', 3)
36+
tx.set('key4', 4)
3737
end
3838
end
3939

4040
assert_raises(Redis::Cluster::TransactionConsistencyError) do
41-
redis.multi do |r|
42-
r.mset('key1', 1, 'key2', 2)
43-
r.mset('key3', 3, 'key4', 4)
41+
redis.multi do |tx|
42+
tx.mset('key1', 1, 'key2', 2)
43+
tx.mset('key3', 3, 'key4', 4)
4444
end
4545
end
4646

@@ -63,10 +63,50 @@ def test_cluster_client_does_support_transaction_with_optimistic_locking
6363
another.resume
6464
v1 = redis.get('{key}1')
6565
v2 = redis.get('{key}2')
66-
tx.call('SET', '{key}1', v2)
67-
tx.call('SET', '{key}2', v1)
66+
tx.set('{key}1', v2)
67+
tx.set('{key}2', v1)
6868
end
6969

7070
assert_equal %w[3 4], redis.mget('{key}1', '{key}2')
7171
end
72+
73+
def test_cluster_client_can_be_used_compatible_with_standalone_client
74+
redis.set('{my}key', 'value')
75+
redis.set('{my}counter', '0')
76+
redis.watch('{my}key', '{my}counter') do |client|
77+
if redis.get('{my}key') == 'value'
78+
client.multi do |tx|
79+
tx.set('{my}key', 'updated value')
80+
tx.incr('{my}counter')
81+
end
82+
else
83+
client.unwatch
84+
end
85+
end
86+
87+
assert_equal('updated value', redis.get('{my}key'))
88+
assert_equal('1', redis.get('{my}counter'))
89+
90+
another = Fiber.new do
91+
cli = build_another_client
92+
cli.set('{my}key', 'another value')
93+
cli.close
94+
Fiber.yield
95+
end
96+
97+
redis.watch('{my}key', '{my}counter') do |client|
98+
another.resume
99+
if redis.get('{my}key') == 'value'
100+
client.multi do |tx|
101+
tx.set('{my}key', 'latest value')
102+
tx.incr('{my}counter')
103+
end
104+
else
105+
client.unwatch
106+
end
107+
end
108+
109+
assert_equal('another value', redis.get('{my}key'))
110+
assert_equal('1', redis.get('{my}counter'))
111+
end
72112
end

cluster/test/commands_on_transactions_test.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,23 @@ def test_watch
4242

4343
assert_raises(Redis::Cluster::TransactionConsistencyError) do
4444
redis.watch('key1', 'key2') do |tx|
45-
tx.call('SET', 'key1', '1')
46-
tx.call('SET', 'key2', '2')
45+
tx.set('key1', '1')
46+
tx.set('key2', '2')
4747
end
4848
end
4949

5050
assert_raises(Redis::Cluster::TransactionConsistencyError) do
5151
redis.watch('{hey}1', '{hey}2') do |tx|
52-
tx.call('SET', '{key}1', '1')
53-
tx.call('SET', '{key}2', '2')
52+
tx.set('{key}1', '1')
53+
tx.set('{key}2', '2')
5454
end
5555
end
5656

5757
assert_empty(redis.watch('{key}1', '{key}2') {})
5858

5959
redis.watch('{key}1', '{key}2') do |tx|
60-
tx.call('SET', '{key}1', '1')
61-
tx.call('SET', '{key}2', '2')
60+
tx.set('{key}1', '1')
61+
tx.set('{key}2', '2')
6262
end
6363

6464
assert_equal %w[1 2], redis.mget('{key}1', '{key}2')

0 commit comments

Comments
 (0)