Skip to content

Commit 88c0c21

Browse files
authored
fix: temporarily remove an experimental feature for the next patch release to prevent to lead to misunderstanding (#320)
1 parent c05234a commit 88c0c21

File tree

4 files changed

+120
-249
lines changed

4 files changed

+120
-249
lines changed

README.md

+76-62
Original file line numberDiff line numberDiff line change
@@ -172,87 +172,101 @@ cli.call('MGET', '{key}1', '{key}2', '{key}3')
172172
## Transactions
173173
This gem supports [Redis transactions](https://redis.io/topics/transactions), including atomicity with `MULTI`/`EXEC`,
174174
and conditional execution with `WATCH`. Redis does not support cross-node transactions, so all keys used within a
175-
transaction must live in the same key slot. To use transactions, you must thus "pin" your client to a single connection using
176-
`#with`. You can pass a single key, in order to perform multiple operations atomically on the same key, like so:
175+
transaction must live in the same key slot. To use transactions, you can use `#multi` method same as the [redis-client](https://github.com/redis-rb/redis-client#usage):
177176

178177
```ruby
179-
cli.with(key: 'my_cool_key') do |conn|
180-
conn.multi do |m|
181-
m.call('INC', 'my_cool_key')
182-
m.call('INC', 'my_cool_key')
183-
end
184-
# my_cool_key will be incremented by 2, with no intermediate state visible to other clients
178+
conn.multi do |tx|
179+
tx.call('INCR', 'my_key')
180+
tx.call('INCR', 'my_key')
185181
end
186182
```
187183

188-
More commonly, however, you will want to perform transactions across multiple keys. To do this, you need to ensure that all keys used in the transaction hash to the same slot; Redis a mechanism called [hashtags](https://redis.io/docs/reference/cluster-spec/#hash-tags) to achieve this. If a key contains a hashag (e.g. in the key `{foo}bar`, the hashtag is `foo`), then it is guaranted to hash to the same slot (and thus always live on the same node) as other keys which contain the same hashtag.
184+
More commonly, however, you will want to perform transactions across multiple keys. To do this,
185+
you need to ensure that all keys used in the transaction hash to the same slot;
186+
Redis a mechanism called [hashtags](https://redis.io/docs/reference/cluster-spec/#hash-tags) to achieve this.
187+
If a key contains a hashag (e.g. in the key `{foo}bar`, the hashtag is `foo`),
188+
then it is guaranted to hash to the same slot (and thus always live on the same node) as other keys which contain the same hashtag.
189189

190-
So, whilst it's not possible in Redis cluster to perform a transction on the keys `foo` and `bar`, it _is_ possible to perform a transaction on the keys `{tag}foo` and `{tag}bar`. To perform such transactions on this gem, pass `hashtag:` to `#with` instead of `key`:
190+
So, whilst it's not possible in Redis cluster to perform a transction on the keys `foo` and `bar`,
191+
it _is_ possible to perform a transaction on the keys `{tag}foo` and `{tag}bar`.
192+
To perform such transactions on this gem, use `hashtag:
191193

192194
```ruby
193-
cli.with(hashtag: 'user123') do |conn|
194-
# You can use any key which contains "{user123}" in this block
195-
conn.multi do |m|
196-
m.call('INC', '{user123}coins_spent')
197-
m.call('DEC', '{user123}coins_available')
198-
end
195+
conn.multi do |tx|
196+
tx.call('INCR', '{user123}coins_spent')
197+
tx.call('DECR', '{user123}coins_available')
199198
end
200199
```
201200

202-
Once you have pinned a client to a particular slot, you can use the same transaction APIs as the
203-
[redis-client](https://github.com/redis-rb/redis-client#usage) gem allows.
204201
```ruby
205-
# No concurrent client will ever see the value 1 in 'mykey'; it will see either zero or two.
206-
cli.call('SET', 'key', 0)
207-
cli.with(key: 'key') do |conn|
208-
conn.multi do |txn|
209-
txn.call('INCR', 'key')
210-
txn.call('INCR', 'key')
211-
end
212-
#=> ['OK', 'OK']
213-
end
214202
# Conditional execution with WATCH can be used to e.g. atomically swap two keys
215203
cli.call('MSET', '{myslot}1', 'v1', '{myslot}2', 'v2')
216-
cli.with(hashtag: 'myslot') do |conn|
217-
conn.call('WATCH', '{myslot}1', '{myslot}2')
218-
conn.multi do |txn|
219-
old_key1 = conn.call('GET', '{myslot}1')
220-
old_key2 = conn.call('GET', '{myslot}2')
221-
txn.call('SET', '{myslot}1', old_key2)
222-
txn.call('SET', '{myslot}2', old_key1)
223-
end
224-
# This transaction will swap the values of {myslot}1 and {myslot}2 only if no concurrent connection modified
225-
# either of the values
226-
end
227-
# You can also pass watch: to #multi as a shortcut
228-
cli.call('MSET', '{myslot}1', 'v1', '{myslot}2', 'v2')
229-
cli.with(hashtag: 'myslot') do |conn|
230-
conn.multi(watch: ['{myslot}1', '{myslot}2']) do |txn|
231-
old_key1, old_key2 = conn.call('MGET', '{myslot}1', '{myslot}2')
232-
txn.call('MSET', '{myslot}1', old_key2, '{myslot}2', old_key1)
233-
end
204+
conn.multi(watch: %w[{myslot}1 {myslot}2]) do |txn|
205+
old_key1 = cli.call('GET', '{myslot}1')
206+
old_key2 = cli.call('GET', '{myslot}2')
207+
txn.call('SET', '{myslot}1', old_key2)
208+
txn.call('SET', '{myslot}2', old_key1)
234209
end
210+
# This transaction will swap the values of {myslot}1 and {myslot}2 only if no concurrent connection modified
211+
# either of the values
235212
```
236213

237-
Pinned connections are aware of redirections and node failures like ordinary calls to `RedisClient::Cluster`, but because
238-
you may have written non-idempotent code inside your block, the block is not automatically retried if e.g. the slot
239-
it is operating on moves to a different node. If you want this, you can opt-in to retries by passing nonzero
240-
`retry_count` to `#with`.
241-
```ruby
242-
cli.with(hashtag: 'myslot', retry_count: 1) do |conn|
243-
conn.call('GET', '{myslot}1')
244-
#=> "value1"
245-
# Now, some changes in cluster topology mean that {key} is moved to a different node!
246-
conn.call('GET', '{myslot}2')
247-
#=> MOVED 9039 127.0.0.1:16381 (RedisClient::CommandError)
248-
# Luckily, the block will get retried (once) and so both GETs will be re-executed on the newly-discovered
249-
# correct node.
250-
end
214+
`RedisClient::Cluster#multi` is aware of redirections and node failures like ordinary calls to `RedisClient::Cluster`,
215+
but because you may have written non-idempotent code inside your block, the block is called once if e.g. the slot
216+
it is operating on moves to a different node.
217+
218+
#### IMO
219+
220+
https://redis.io/docs/interact/transactions/#errors-inside-a-transaction
221+
222+
> Errors happening after EXEC instead are not handled in a special way: all the other commands will be executed even if some command fails during the transaction.
223+
> It's important to note that even when a command fails, all the other commands in the queue are processed - Redis will not stop the processing of commands.
224+
251225
```
226+
$ telnet 127.0.0.1 6379
227+
set key3 a
228+
+OK
229+
multi
230+
+OK
231+
set key3 b
232+
+QUEUED
233+
incr key3
234+
+QUEUED
235+
exec
236+
*2
237+
+OK
238+
-ERR value is not an integer or out of range
239+
get key3
240+
$1
241+
b
242+
```
243+
244+
The `SET` command was processed because the `INCR` command was queued.
245+
246+
```
247+
multi
248+
+OK
249+
set key3 c
250+
+QUEUED
251+
mybad key3 d
252+
-ERR unknown command 'mybad', with args beginning with: 'key3' 'd'
253+
exec
254+
-EXECABORT Transaction discarded because of previous errors.
255+
get key3
256+
$1
257+
b
258+
```
259+
260+
The `SET` command wasn't processed because of the error during the queueing.
261+
262+
https://redis.io/docs/interact/transactions/#what-about-rollbacks
263+
264+
> Redis does not support rollbacks of transactions since supporting rollbacks would have a significant impact on the simplicity and performance of Redis.
252265
253-
Because `RedisClient` from the redis-client gem implements `#with` as simply `yield self` and ignores all of its
254-
arguments, it's possible to write code which is compatible with both redis-client and redis-cluster-client; the `#with`
255-
call will pin the connection to a slot when using clustering, or be a no-op when not.
266+
It's hard to validate them perfectly in advance on the client side.
267+
It seems that Redis aims to prior simplicity and performance efficiency.
268+
So I think it's wrong to use the transaction feature by complex ways.
269+
To say nothing of the cluster mode because of the CAP theorem. Redis is just a key-value store.
256270

257271
## ACL
258272
The cluster client internally calls [COMMAND](https://redis.io/commands/command/) and [CLUSTER NODES](https://redis.io/commands/cluster-nodes/) commands to operate correctly.

lib/redis_client/cluster.rb

+9-9
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
require 'redis_client/cluster/pub_sub'
66
require 'redis_client/cluster/router'
77
require 'redis_client/cluster/transaction'
8+
require 'redis_client/cluster/pinning_node'
89

910
class RedisClient
1011
class Cluster
@@ -95,19 +96,18 @@ def multi(watch: nil)
9596
transaction.execute
9697
end
9798

98-
def with(key: nil, hashtag: nil, write: true, retry_count: 0, &block)
99+
def pubsub
100+
::RedisClient::Cluster::PubSub.new(@router, @command_builder)
101+
end
102+
103+
# TODO: This isn't an official public interface yet. Don't use in your production environment.
104+
# @see https://github.com/redis-rb/redis-cluster-client/issues/299
105+
def with(key: nil, hashtag: nil, write: true, _retry_count: 0, &_)
99106
key = process_with_arguments(key, hashtag)
100107

101108
node_key = @router.find_node_key_by_key(key, primary: write)
102109
node = @router.find_node(node_key)
103-
# Calling #with checks out the underlying connection if this is a pooled connection
104-
# Calling it through #try_delegate ensures we handle any redirections and retry the entire
105-
# transaction if so.
106-
@router.try_delegate(node, :with, retry_count: retry_count, &block)
107-
end
108-
109-
def pubsub
110-
::RedisClient::Cluster::PubSub.new(@router, @command_builder)
110+
yield ::RedisClient::Cluster::PinningNode.new(node)
111111
end
112112

113113
def close
+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
class RedisClient
4+
class Cluster
5+
class PinningNode
6+
def initialize(client)
7+
@client = client
8+
end
9+
10+
def call(*args, **kwargs, &block)
11+
@client.call(*args, **kwargs, &block)
12+
end
13+
14+
def call_v(args, &block)
15+
@client.call_v(args, &block)
16+
end
17+
18+
def call_once(*args, **kwargs, &block)
19+
@client.call_once(*args, **kwargs, &block)
20+
end
21+
22+
def call_once_v(args, &block)
23+
@client.call_once_v(args, &block)
24+
end
25+
26+
def blocking_call(timeout, *args, **kwargs, &block)
27+
@client.blocking_call(timeout, *args, **kwargs, &block)
28+
end
29+
30+
def blocking_call_v(timeout, args, &block)
31+
@client.blocking_call_v(timeout, args, &block)
32+
end
33+
end
34+
end
35+
end

0 commit comments

Comments
 (0)