Skip to content

Commit c4e6dcc

Browse files
authored
Merge pull request #1270 from supercaracal/fix-cluster-cas-pattern
Fix a cluster client interface for CAS operations to be more compatible with standalone client
2 parents a06456c + 30da9c4 commit c4e6dcc

6 files changed

+107
-43
lines changed

cluster/README.md

+6-7
Original file line numberDiff line numberDiff line change
@@ -82,9 +82,11 @@ Client libraries can make them compatible up to a point, but a part of features
8282
Especially, some cautions are needed to use the transaction feature with an optimistic locking.
8383

8484
```ruby
85-
redis.watch("{my}key") do |client| # The client is an instance of the internal adapter
86-
if redis.get("{my}key") == "some value" # We can't use the client passed by the block argument
87-
client.multi do |tx| # The tx is the same instance of the internal adapter
85+
# The client is an instance of the internal adapter for the optimistic locking
86+
redis.watch("{my}key") do |client|
87+
if client.get("{my}key") == "some value"
88+
# The tx is an instance of the internal adapter for the transaction
89+
client.multi do |tx|
8890
tx.set("{my}key", "other value")
8991
tx.incr("{my}counter")
9092
end
@@ -95,8 +97,5 @@ end
9597
```
9698

9799
In a cluster mode client, you need to pass a block if you call the watch method and you need to specify an argument to the block.
98-
Also, you should use the block argument as a receiver to call the transaction feature methods in the block.
99-
The commands called by methods of the receiver are added to the internal pipeline for the transaction and they are sent to the server lastly.
100-
On the other hand, if you want to call other methods for commands, you can use the global instance of the client instead of the block argument.
101-
It affects out of the transaction pipeline and the replies are returned soon.
100+
Also, you should use the block argument as a receiver to call commands in the block.
102101
Although the above restrictions are needed, this implementations is compatible with a standalone client.

cluster/lib/redis/cluster.rb

+7-6
Original file line numberDiff line numberDiff line change
@@ -99,19 +99,20 @@ def cluster(subcommand, *args)
9999
# Watch the given keys to determine execution of the MULTI/EXEC block.
100100
#
101101
# Using a block is required for a cluster client. It's different from a standalone client.
102-
# And you should use the block argument as a receiver if you call transaction feature methods.
103-
# On the other hand, you can use the global instance of the client if you call methods of other commands.
102+
# And you should use the block argument as a receiver if you call commands.
104103
#
105104
# An `#unwatch` is automatically issued if an exception is raised within the
106105
# block that is a subclass of StandardError and is not a ConnectionError.
107106
#
108107
# @param keys [String, Array<String>] one or more keys to watch
109-
# @return [Array<Object>] replies of the transaction or an empty array
108+
# @return [Object] returns the return value of the block
110109
#
111110
# @example A typical use case.
112-
# redis.watch("{my}key") do |client| # The client is an instance of the internal adapter
113-
# if redis.get("{my}key") == "some value" # We can't use the client passed by the block argument
114-
# client.multi do |tx| # The tx is the same instance of the internal adapter
111+
# # The client is an instance of the internal adapter for the optimistic locking
112+
# redis.watch("{my}key") do |client|
113+
# if client.get("{my}key") == "some value"
114+
# # The tx is an instance of the internal adapter for the transaction
115+
# client.multi do |tx|
115116
# tx.set("{my}key", "other value")
116117
# tx.incr("{my}counter")
117118
# end

cluster/lib/redis/cluster/client.rb

+4-2
Original file line numberDiff line numberDiff line change
@@ -119,8 +119,10 @@ def watch(*keys, &block)
119119
transaction = Redis::Cluster::TransactionAdapter.new(
120120
self, @router, @command_builder, node: c, slot: slot, asking: asking
121121
)
122-
yield transaction
123-
transaction.execute
122+
123+
result = yield transaction
124+
c.call('UNWATCH') unless transaction.lock_released?
125+
result
124126
end
125127
end
126128
end

cluster/lib/redis/cluster/transaction_adapter.rb

+62-9
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,69 @@
44

55
class Redis
66
class Cluster
7-
class TransactionAdapter < RedisClient::Cluster::Transaction
7+
class TransactionAdapter
8+
class Internal < RedisClient::Cluster::Transaction
9+
def initialize(client, router, command_builder, node: nil, slot: nil, asking: false)
10+
@client = client
11+
super(router, command_builder, node: node, slot: slot, asking: asking)
12+
end
13+
14+
def multi
15+
raise(Redis::Cluster::TransactionConsistencyError, "Can't nest multi transaction")
16+
end
17+
18+
def exec
19+
# no need to do anything
20+
end
21+
22+
def discard
23+
# no need to do anything
24+
end
25+
26+
def watch(*_)
27+
raise(Redis::Cluster::TransactionConsistencyError, "Can't use watch in a transaction")
28+
end
29+
30+
def unwatch
31+
# no need to do anything
32+
end
33+
34+
private
35+
36+
def method_missing(name, *args, **kwargs, &block)
37+
return call(name, *args, **kwargs, &block) if @client.respond_to?(name)
38+
39+
super
40+
end
41+
42+
def respond_to_missing?(name, include_private = false)
43+
return true if @client.respond_to?(name)
44+
45+
super
46+
end
47+
end
48+
849
def initialize(client, router, command_builder, node: nil, slot: nil, asking: false)
950
@client = client
10-
super(router, command_builder, node: node, slot: slot, asking: asking)
51+
@router = router
52+
@command_builder = command_builder
53+
@node = node
54+
@slot = slot
55+
@asking = asking
56+
@lock_released = false
57+
end
58+
59+
def lock_released?
60+
@lock_released
1161
end
1262

1363
def multi
14-
yield self
64+
@lock_released = true
65+
transaction = Redis::Cluster::TransactionAdapter::Internal.new(
66+
@client, @router, @command_builder, node: @node, slot: @slot, asking: @asking
67+
)
68+
yield transaction
69+
transaction.execute
1570
end
1671

1772
def exec
@@ -23,20 +78,18 @@ def discard
2378
end
2479

2580
def watch(*_)
26-
raise(
27-
Redis::Cluster::TransactionConsistencyError,
28-
'You should pass all the keys to a watch method if you use the cluster client.'
29-
)
81+
raise(Redis::Cluster::TransactionConsistencyError, "Can't nest watch command if you use the cluster client")
3082
end
3183

3284
def unwatch
33-
# no need to do anything
85+
@lock_released = true
86+
@node.call('UNWATCH')
3487
end
3588

3689
private
3790

3891
def method_missing(name, *args, **kwargs, &block)
39-
return call(name, *args, **kwargs, &block) if @client.respond_to?(name)
92+
return @client.public_send(name, *args, **kwargs, &block) if @client.respond_to?(name)
4093

4194
super
4295
end

cluster/test/client_transactions_test.rb

+10-7
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,15 @@ def test_cluster_client_does_support_transaction_with_optimistic_locking
5959
Fiber.yield
6060
end
6161

62-
redis.watch('{key}1', '{key}2') do |tx|
62+
redis.watch('{key}1', '{key}2') do |client|
6363
another.resume
64-
v1 = redis.get('{key}1')
65-
v2 = redis.get('{key}2')
66-
tx.set('{key}1', v2)
67-
tx.set('{key}2', v1)
64+
v1 = client.get('{key}1')
65+
v2 = client.get('{key}2')
66+
67+
client.multi do |tx|
68+
tx.set('{key}1', v2)
69+
tx.set('{key}2', v1)
70+
end
6871
end
6972

7073
assert_equal %w[3 4], redis.mget('{key}1', '{key}2')
@@ -74,7 +77,7 @@ def test_cluster_client_can_be_used_compatible_with_standalone_client
7477
redis.set('{my}key', 'value')
7578
redis.set('{my}counter', '0')
7679
redis.watch('{my}key', '{my}counter') do |client|
77-
if redis.get('{my}key') == 'value'
80+
if client.get('{my}key') == 'value'
7881
client.multi do |tx|
7982
tx.set('{my}key', 'updated value')
8083
tx.incr('{my}counter')
@@ -96,7 +99,7 @@ def test_cluster_client_can_be_used_compatible_with_standalone_client
9699

97100
redis.watch('{my}key', '{my}counter') do |client|
98101
another.resume
99-
if redis.get('{my}key') == 'value'
102+
if client.get('{my}key') == 'value'
100103
client.multi do |tx|
101104
tx.set('{my}key', 'latest value')
102105
tx.incr('{my}counter')

cluster/test/commands_on_transactions_test.rb

+18-12
Original file line numberDiff line numberDiff line change
@@ -45,30 +45,36 @@ def test_watch
4545
end
4646

4747
assert_raises(Redis::Cluster::TransactionConsistencyError) do
48-
redis.watch('{key}1', '{key}2') do |tx|
49-
tx.watch('{key}3')
48+
redis.watch('{key}1', '{key}2') do |cli|
49+
cli.watch('{key}3')
5050
end
5151
end
5252

5353
assert_raises(Redis::Cluster::TransactionConsistencyError) do
54-
redis.watch('key1', 'key2') do |tx|
55-
tx.set('key1', '1')
56-
tx.set('key2', '2')
54+
redis.watch('key1', 'key2') do |cli|
55+
cli.multi do |tx|
56+
tx.set('key1', '1')
57+
tx.set('key2', '2')
58+
end
5759
end
5860
end
5961

6062
assert_raises(Redis::Cluster::TransactionConsistencyError) do
61-
redis.watch('{hey}1', '{hey}2') do |tx|
62-
tx.set('{key}1', '1')
63-
tx.set('{key}2', '2')
63+
redis.watch('{hey}1', '{hey}2') do |cli|
64+
cli.multi do |tx|
65+
tx.set('{key}1', '1')
66+
tx.set('{key}2', '2')
67+
end
6468
end
6569
end
6670

67-
assert_empty(redis.watch('{key}1', '{key}2') { |_| })
71+
assert_equal('hello', redis.watch('{key}1', '{key}2') { |_| 'hello' })
6872

69-
redis.watch('{key}1', '{key}2') do |tx|
70-
tx.set('{key}1', '1')
71-
tx.set('{key}2', '2')
73+
redis.watch('{key}1', '{key}2') do |cli|
74+
cli.multi do |tx|
75+
tx.set('{key}1', '1')
76+
tx.set('{key}2', '2')
77+
end
7278
end
7379

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

0 commit comments

Comments
 (0)