Skip to content

Commit ad5c2c6

Browse files
authored
Add some test cases (#23)
1 parent 133a1ba commit ad5c2c6

File tree

3 files changed

+95
-34
lines changed

3 files changed

+95
-34
lines changed

README.md

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,82 @@
33

44
Redis Cluster Client
55
===============================================================================
6+
This library is a client for Redis cluster.
7+
It depends on [redis-client](https://github.com/redis-rb/redis-client).
8+
So it would be better to read `redis-client` documents first.
69

7-
TODO
10+
## Installation
11+
```ruby
12+
gem 'redis-cluster-client'
13+
```
14+
15+
## Initialization
16+
| key | type | default | description |
17+
| --- | --- |
18+
| `nodes` | `String` or `Array<String, Hash>` | `['redis://127.0.0.1:6379]` | node addresses for startup connection |
19+
| `replica` | Boolean | `false` | `true` if client use scale read feature |
20+
| `fixed_hostname` | `String` | `nil` | specify if client connects to cluster with SSL and single endpoint |
21+
22+
Also, [the other generic options](https://github.com/redis-rb/redis-client#configuration) can be passed.
23+
24+
```ruby
25+
# The following examples are Docker containers on localhost.
26+
# The client first attempts to connect to redis://127.0.0.1:6379 internally.
27+
28+
# To connect to primary nodes only
29+
RedisClient.cluster.new_client
30+
#=> #<RedisClient::Cluster 172.20.0.2:6379, 172.20.0.6:6379, 172.20.0.7:6379>
31+
32+
# To connect to all nodes and to use scale reading feature
33+
RedisClient.cluster(replica: true).new_client
34+
#=> #<RedisClient::Cluster 172.20.0.2:6379, 172.20.0.3:6379, 172.20.0.4:6379, 172.20.0.5:6379, 172.20.0.6:6379, 172.20.0.7:6379>
35+
36+
# With generic options for redis-client
37+
RedisClient.cluster(timeout: 3.0).new_client
38+
```
39+
40+
```ruby
41+
# To connect with subset nodes for startup
42+
RedisClient.cluster(nodes: %w[redis://node1:6379 redis://node2:6379]).new_client
43+
```
844

945
```ruby
10-
cli = RedisClient.cluster(nodes: %w[redis://127.0.0.1:6379]).new_client
46+
# To connect with single endpoint
47+
RedisClient.cluster(nodes: 'redis://endpoint.example.com:6379').new_client
48+
```
1149

12-
cli.call('PING')
13-
#=> PONG
50+
```ruby
51+
# To connect with single endpoint and SSL
52+
RedisClient.cluster(nodes: 'rediss://endpoint.example.com:6379', fixed_hostname: 'endpoint.example.com').new_client
1453
```
54+
55+
## Interfaces
56+
The following methods are able to be used like `redis-client`.
57+
* `#call`
58+
* `#call_once`
59+
* `#blocking_call`
60+
* `#scan`
61+
* `#sscan`
62+
* `#hscan`
63+
* `#zscan`
64+
* `#pipelined`
65+
* `#pubsub`
66+
* `#close`
67+
68+
The other methods are not implemented because the client cannot operate with cluster mode.
69+
`#pipelined` method splits and sends commands to each node and aggregates replies.
70+
71+
## Multiple keys and CROSSSLOT error
72+
A part of commands can be passed multiple keys. But it has a constraint the keys are in the same hash slot.
73+
The following error occurs because keys must be in the same hash slot and not just the same node.
74+
75+
```ruby
76+
RedisClient.cluster.new_client.call('MGET', 'key1', 'key2', 'key3')
77+
#=> CROSSSLOT Keys in request don't hash to the same slot (RedisClient::CommandError)
78+
```
79+
80+
## Connection pooling
81+
TODO
82+
83+
## Connection drivers
84+
TODO

lib/redis_client/cluster.rb

Lines changed: 20 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,19 +20,19 @@ def initialize(client)
2020
end
2121

2222
def call(*command, **kwargs)
23-
node_key = @client.send(:find_node_key, command, primary_only: true)
23+
node_key = @client.send(:find_node_key, *command, primary_only: true)
2424
@grouped[node_key] += [[@size, :call, command, kwargs]]
2525
@size += 1
2626
end
2727

2828
def call_once(*command, **kwargs)
29-
node_key = @client.send(:find_node_key, command, primary_only: true)
29+
node_key = @client.send(:find_node_key, *command, primary_only: true)
3030
@grouped[node_key] += [[@size, :call_once, command, kwargs]]
3131
@size += 1
3232
end
3333

3434
def blocking_call(timeout, *command, **kwargs)
35-
node_key = @client.send(:find_node_key, command, primary_only: true)
35+
node_key = @client.send(:find_node_key, *command, primary_only: true)
3636
@grouped[node_key] += [[@size, :blocking_call, timeout, command, kwargs]]
3737
@size += 1
3838
end
@@ -66,6 +66,14 @@ def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Met
6666
end
6767

6868
ZERO_CURSOR_FOR_SCAN = '0'
69+
CMD_SCAN = 'SCAN'
70+
CMD_SSCAN = 'SSCAN'
71+
CMD_HSCAN = 'HSCAN'
72+
CMD_ZSCAN = 'ZSCAN'
73+
CMD_ASKING = 'ASKING'
74+
REPLY_OK = 'OK'
75+
REPLY_MOVED = 'MOVED'
76+
REPLY_ASK = 'ASK'
6977

7078
def initialize(config, pool: nil, **kwargs)
7179
@config = config.dup
@@ -97,35 +105,27 @@ def scan(*args, **kwargs, &block)
97105

98106
cursor = ZERO_CURSOR_FOR_SCAN
99107
loop do
100-
cursor, keys = _scan('SCAN', cursor, *args, **kwargs)
108+
cursor, keys = _scan(CMD_SCAN, cursor, *args, **kwargs)
101109
keys.each(&block)
102110
break if cursor == ZERO_CURSOR_FOR_SCAN
103111
end
104112
end
105113

106114
def sscan(key, *args, **kwargs, &block)
107-
node = assign_node('SSCAN', key)
115+
node = assign_node(CMD_SSCAN, key)
108116
try_send(node, :sscan, key, *args, **kwargs, &block)
109117
end
110118

111119
def hscan(key, *args, **kwargs, &block)
112-
node = assign_node('HSCAN', key)
120+
node = assign_node(CMD_HSCAN, key)
113121
try_send(node, :hscan, key, *args, **kwargs, &block)
114122
end
115123

116124
def zscan(key, *args, **kwargs, &block)
117-
node = assign_node('ZSCAN', key)
125+
node = assign_node(CMD_ZSCAN, key)
118126
try_send(node, :zscan, key, *args, **kwargs, &block)
119127
end
120128

121-
def mset
122-
# TODO: impl
123-
end
124-
125-
def mget
126-
# TODO: impl
127-
end
128-
129129
def pipelined
130130
pipeline = ::RedisClient::Cluster::Pipeline.new(self)
131131
yield pipeline
@@ -138,15 +138,6 @@ def pubsub
138138
# TODO: impl
139139
end
140140

141-
def size
142-
# TODO: impl
143-
end
144-
145-
def with(options = {})
146-
# TODO: impl
147-
end
148-
alias then with
149-
150141
def close
151142
@node.each(&:close)
152143
nil
@@ -253,17 +244,17 @@ def send_pubsub_command(method, *command, **kwargs, &block) # rubocop:disable Me
253244
def try_send(node, method, *args, retry_count: 3, **kwargs, &block) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
254245
node.send(method, *args, **kwargs, &block)
255246
rescue ::RedisClient::CommandError => e
256-
if e.message.start_with?('MOVED')
247+
if e.message.start_with?(REPLY_MOVED)
257248
raise if retry_count <= 0
258249

259250
node = assign_redirection_node(e.message)
260251
retry_count -= 1
261252
retry
262-
elsif e.message.start_with?('ASK')
253+
elsif e.message.start_with?(REPLY_ASK)
263254
raise if retry_count <= 0
264255

265256
node = assign_asking_node(e.message)
266-
node.call('ASKING')
257+
node.call(CMD_ASKING)
267258
retry_count -= 1
268259
retry
269260
else
@@ -309,11 +300,11 @@ def assign_asking_node(err_msg)
309300
end
310301

311302
def assign_node(*command)
312-
node_key = find_node_key(command)
303+
node_key = find_node_key(*command)
313304
find_node(node_key)
314305
end
315306

316-
def find_node_key(command, primary_only: false)
307+
def find_node_key(*command, primary_only: false)
317308
key = @command.extract_first_key(command)
318309
return if key.empty?
319310

lib/redis_client/cluster/node_key.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def split(node_key)
1919
pos = node_key&.rindex(DELIMITER, -1)
2020
return [node_key, nil] if pos.nil?
2121

22-
[node_key[0, pos], node_key[pos + 1, node_key.size - pos - 1]]
22+
[node_key[0, pos], node_key[(pos + 1)..]]
2323
end
2424

2525
def build_from_uri(uri)

0 commit comments

Comments
 (0)