Skip to content

Commit f236b12

Browse files
authored
Merge pull request #1268 from supercaracal/fix-cluster-client
Add some documents for the transaction feature in the cluster client
2 parents 6aa6b6b + aea43f6 commit f236b12

File tree

5 files changed

+70
-10
lines changed

5 files changed

+70
-10
lines changed

cluster/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,28 @@ Redis::Cluster.new(nodes: %w[rediss://foo-endpoint.example.com:6379], fixed_host
7575
```
7676

7777
In case of the above architecture, if you don't pass the `fixed_hostname` option to the client and servers return IP addresses of nodes, the client may fail to verify certificates.
78+
79+
## Transaction with an optimistic locking
80+
Since Redis cluster is a distributed system, several behaviors are different from a standalone server.
81+
Client libraries can make them compatible up to a point, but a part of features needs some restrictions.
82+
Especially, some cautions are needed to use the transaction feature with an optimistic locking.
83+
84+
```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
88+
tx.set("{my}key", "other value")
89+
tx.incr("{my}counter")
90+
end
91+
else
92+
client.unwatch
93+
end
94+
end
95+
```
96+
97+
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.
102+
Although the above restrictions are needed, this implementations is compatible with a standalone client.

cluster/lib/redis/cluster.rb

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -96,18 +96,30 @@ def cluster(subcommand, *args)
9696
send_command([:cluster, subcommand] + args, &block)
9797
end
9898

99+
# Watch the given keys to determine execution of the MULTI/EXEC block.
100+
#
101+
# 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.
104+
#
105+
# An `#unwatch` is automatically issued if an exception is raised within the
106+
# block that is a subclass of StandardError and is not a ConnectionError.
107+
#
108+
# @param keys [String, Array<String>] one or more keys to watch
109+
# @return [Array<Object>] replies of the transaction or an empty array
110+
#
99111
# @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")
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
115+
# tx.set("{my}key", "other value")
116+
# tx.incr("{my}counter")
105117
# end
106118
# else
107119
# client.unwatch
108120
# end
109121
# end
110-
# # => ["OK", 6]
122+
# #=> ["OK", 6]
111123
def watch(*keys, &block)
112124
synchronize { |c| c.watch(*keys, &block) }
113125
end

cluster/lib/redis/cluster/client.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,9 +99,19 @@ def multi(watch: nil, &block)
9999
handle_errors { super(watch: watch, &block) }
100100
end
101101

102-
def watch(*keys)
102+
def watch(*keys, &block)
103103
unless block_given?
104-
raise Redis::Cluster::TransactionConsistencyError, 'A block is required if you use the cluster client.'
104+
raise(
105+
Redis::Cluster::TransactionConsistencyError,
106+
'A block is required if you use the cluster client.'
107+
)
108+
end
109+
110+
unless block.arity == 1
111+
raise(
112+
Redis::Cluster::TransactionConsistencyError,
113+
'Given block needs an argument if you use the cluster client.'
114+
)
105115
end
106116

107117
handle_errors do

cluster/lib/redis/cluster/transaction_adapter.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ def discard
2323
end
2424

2525
def watch(*_)
26-
# no need to do anything
26+
raise(
27+
Redis::Cluster::TransactionConsistencyError,
28+
'You should pass all the keys to a watch method if you use the cluster client.'
29+
)
2730
end
2831

2932
def unwatch

cluster/test/commands_on_transactions_test.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,16 @@ def test_watch
4040
redis.watch('{key}1', '{key}2')
4141
end
4242

43+
assert_raises(Redis::Cluster::TransactionConsistencyError) do
44+
redis.watch('{key}1', '{key}2') {}
45+
end
46+
47+
assert_raises(Redis::Cluster::TransactionConsistencyError) do
48+
redis.watch('{key}1', '{key}2') do |tx|
49+
tx.watch('{key}3')
50+
end
51+
end
52+
4353
assert_raises(Redis::Cluster::TransactionConsistencyError) do
4454
redis.watch('key1', 'key2') do |tx|
4555
tx.set('key1', '1')
@@ -54,7 +64,7 @@ def test_watch
5464
end
5565
end
5666

57-
assert_empty(redis.watch('{key}1', '{key}2') {})
67+
assert_empty(redis.watch('{key}1', '{key}2') { |_| })
5868

5969
redis.watch('{key}1', '{key}2') do |tx|
6070
tx.set('{key}1', '1')

0 commit comments

Comments
 (0)