redis-cluster-client 0.16.4 → 0.16.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/redis_client/cluster/command.rb +86 -24
- data/lib/redis_client/cluster/node.rb +23 -12
- data/lib/redis_client/cluster/optimistic_locking.rb +2 -1
- data/lib/redis_client/cluster/pipeline.rb +1 -4
- data/lib/redis_client/cluster/pub_sub.rb +35 -44
- data/lib/redis_client/cluster/router.rb +105 -51
- data/lib/redis_client/cluster/transaction.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 201f719d07b33c0df8cde529564564e2993495a7817419730b6699047e40c068
|
|
4
|
+
data.tar.gz: 45790a9f41cd3708e8ffc7c2729e767204711c966cf9dd19aed419b7abc9fb5c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '085310367927882a2e2d6547419ca4edc254ea2296411ed0c8f0f7352b48d98c54a6e005cc4e11ab0b5455810fa9b086dfefa9786373f926de520c7972bae5fe'
|
|
7
|
+
data.tar.gz: 5d3db4f8ecee72af9d44c239bab164c581b4983c21a7eca1063d3724f0f70d1ed5ef73d5b141a43d34a32edc6419210fd839c0d48b61efac18ac88d5306a5a03
|
|
@@ -9,15 +9,25 @@ class RedisClient
|
|
|
9
9
|
class Command
|
|
10
10
|
EMPTY_STRING = ''
|
|
11
11
|
EMPTY_HASH = {}.freeze
|
|
12
|
+
EMPTY_POLICIES = [nil, nil].freeze
|
|
13
|
+
REQUEST_POLICY_PREFIX = 'request_policy:'
|
|
14
|
+
RESPONSE_POLICY_PREFIX = 'response_policy:'
|
|
15
|
+
SUBCOMMAND_DELIMITER = '|'
|
|
12
16
|
|
|
13
|
-
private_constant :EMPTY_HASH
|
|
17
|
+
private_constant :EMPTY_HASH, :EMPTY_POLICIES, :REQUEST_POLICY_PREFIX,
|
|
18
|
+
:RESPONSE_POLICY_PREFIX, :SUBCOMMAND_DELIMITER
|
|
14
19
|
|
|
20
|
+
# @see https://redis.io/docs/latest/commands/command/ The reply of the COMMAND command
|
|
21
|
+
# @see https://redis.io/docs/latest/develop/reference/command-tips/ Command tips
|
|
15
22
|
Spec = Struct.new(
|
|
16
23
|
'RedisCommandSpec',
|
|
17
24
|
:first_key_position,
|
|
18
25
|
:key_step,
|
|
19
26
|
:write?,
|
|
20
27
|
:readonly?,
|
|
28
|
+
:request_policy,
|
|
29
|
+
:response_policy,
|
|
30
|
+
:subcommands,
|
|
21
31
|
keyword_init: true
|
|
22
32
|
) do
|
|
23
33
|
def extract_first_key(command)
|
|
@@ -86,40 +96,92 @@ class RedisClient
|
|
|
86
96
|
|
|
87
97
|
private
|
|
88
98
|
|
|
89
|
-
def parse_command_reply(rows)
|
|
99
|
+
def parse_command_reply(rows)
|
|
90
100
|
rows&.each_with_object({}) do |row, acc|
|
|
91
101
|
next if row.first.nil?
|
|
92
102
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
pos = case row.first
|
|
96
|
-
when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
|
|
97
|
-
when 'object', 'xgroup' then 2
|
|
98
|
-
when 'migrate', 'xread', 'xreadgroup' then 0
|
|
99
|
-
else row[3]
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
writable = case row.first
|
|
103
|
-
when 'xgroup' then true
|
|
104
|
-
else row[2].include?('write')
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
acc[row.first] = ::RedisClient::Cluster::Command::Spec.new(
|
|
108
|
-
first_key_position: pos,
|
|
109
|
-
key_step: row[5],
|
|
110
|
-
write?: writable,
|
|
111
|
-
readonly?: row[2].include?('readonly')
|
|
112
|
-
)
|
|
103
|
+
acc[row.first] = build_spec(row)
|
|
113
104
|
end.freeze || EMPTY_HASH
|
|
114
105
|
end
|
|
106
|
+
|
|
107
|
+
def build_spec(row)
|
|
108
|
+
request_policy, response_policy = parse_tips(row[7])
|
|
109
|
+
|
|
110
|
+
::RedisClient::Cluster::Command::Spec.new(
|
|
111
|
+
first_key_position: parse_first_key_position(row),
|
|
112
|
+
key_step: row[5],
|
|
113
|
+
write?: parse_writability(row),
|
|
114
|
+
readonly?: row[2].include?('readonly'),
|
|
115
|
+
request_policy: request_policy,
|
|
116
|
+
response_policy: response_policy,
|
|
117
|
+
subcommands: parse_subcommands(row[9])
|
|
118
|
+
).freeze
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# The redis 6.2 or earlier doesn't include the information of the subcommands in the reply.
|
|
122
|
+
# These hard-coded positions are the fallback for such old versions.
|
|
123
|
+
def parse_first_key_position(row)
|
|
124
|
+
case row.first
|
|
125
|
+
when 'eval', 'evalsha', 'zinterstore', 'zunionstore' then 3
|
|
126
|
+
when 'object', 'xgroup' then 2
|
|
127
|
+
when 'migrate', 'xread', 'xreadgroup' then 0
|
|
128
|
+
else row[3]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def parse_writability(row)
|
|
133
|
+
case row.first
|
|
134
|
+
when 'xgroup' then true
|
|
135
|
+
else row[2].include?('write')
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# The command tips are available in the redis 7.0 or later.
|
|
140
|
+
def parse_tips(tips)
|
|
141
|
+
return EMPTY_POLICIES if tips.nil? || tips.empty?
|
|
142
|
+
|
|
143
|
+
request_policy = response_policy = nil
|
|
144
|
+
|
|
145
|
+
tips.each do |tip|
|
|
146
|
+
if tip.start_with?(REQUEST_POLICY_PREFIX)
|
|
147
|
+
request_policy = -tip[REQUEST_POLICY_PREFIX.size..]
|
|
148
|
+
elsif tip.start_with?(RESPONSE_POLICY_PREFIX)
|
|
149
|
+
response_policy = -tip[RESPONSE_POLICY_PREFIX.size..]
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
[request_policy, response_policy]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The information of the subcommands is available in the redis 7.0 or later.
|
|
157
|
+
# A container command such as XINFO reports its key positions per subcommand.
|
|
158
|
+
def parse_subcommands(rows)
|
|
159
|
+
return if rows.nil? || rows.empty?
|
|
160
|
+
|
|
161
|
+
rows.each_with_object({}) do |row, acc|
|
|
162
|
+
name = row.first
|
|
163
|
+
next if name.nil?
|
|
164
|
+
|
|
165
|
+
# The server replies with a full name of the subcommand such as `xinfo|stream`.
|
|
166
|
+
i = name.index(SUBCOMMAND_DELIMITER)
|
|
167
|
+
acc[i.nil? ? name : name[(i + 1)..]] = build_spec(row)
|
|
168
|
+
end.freeze
|
|
169
|
+
end
|
|
115
170
|
end
|
|
116
171
|
|
|
117
172
|
def initialize(commands)
|
|
118
173
|
@commands = commands || EMPTY_HASH
|
|
119
174
|
end
|
|
120
175
|
|
|
121
|
-
def get_spec(
|
|
122
|
-
|
|
176
|
+
def get_spec(command) # rubocop:disable Metrics/AbcSize
|
|
177
|
+
name = command.first
|
|
178
|
+
spec = @commands[name] || @commands[name.to_s.downcase(:ascii)]
|
|
179
|
+
return spec if spec.nil? || spec.subcommands.nil?
|
|
180
|
+
|
|
181
|
+
subcommand = command[1]
|
|
182
|
+
return spec if subcommand.nil?
|
|
183
|
+
|
|
184
|
+
spec.subcommands[subcommand] || spec.subcommands[subcommand.to_s.downcase(:ascii)] || spec
|
|
123
185
|
end
|
|
124
186
|
|
|
125
187
|
def exists?(name)
|
|
@@ -53,15 +53,10 @@ class RedisClient
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
class CharArray
|
|
56
|
-
BASE = ''
|
|
57
|
-
PADDING = '0'
|
|
58
|
-
|
|
59
|
-
private_constant :BASE, :PADDING
|
|
60
|
-
|
|
61
56
|
def initialize(size, elements)
|
|
62
57
|
@elements = elements
|
|
63
|
-
@string = String.new(
|
|
64
|
-
size.times { @string <<
|
|
58
|
+
@string = String.new('', encoding: Encoding::BINARY, capacity: size)
|
|
59
|
+
size.times { @string << 0xff }
|
|
65
60
|
end
|
|
66
61
|
|
|
67
62
|
def [](index)
|
|
@@ -146,6 +141,16 @@ class RedisClient
|
|
|
146
141
|
call_multiple_nodes!(@topology.replica_clients, method, command, args, &block)
|
|
147
142
|
end
|
|
148
143
|
|
|
144
|
+
# for the `response_policy:one_succeeded` command tip
|
|
145
|
+
def call_all_leniently(method, command, args, &block)
|
|
146
|
+
call_multiple_nodes_leniently(@topology.clients, method, command, args, &block)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# for the `response_policy:one_succeeded` command tip
|
|
150
|
+
def call_primaries_leniently(method, command, args, &block)
|
|
151
|
+
call_multiple_nodes_leniently(@topology.primary_clients, method, command, args, &block)
|
|
152
|
+
end
|
|
153
|
+
|
|
149
154
|
def send_ping(method, command, args, &block)
|
|
150
155
|
result_values, errors = call_multiple_nodes(@topology.clients, method, command, args, &block)
|
|
151
156
|
return result_values if errors.nil? || errors.empty?
|
|
@@ -270,6 +275,13 @@ class RedisClient
|
|
|
270
275
|
raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors)
|
|
271
276
|
end
|
|
272
277
|
|
|
278
|
+
def call_multiple_nodes_leniently(clients, method, command, args, &block)
|
|
279
|
+
result_values, errors = call_multiple_nodes(clients, method, command, args, &block)
|
|
280
|
+
return result_values unless result_values.nil? || result_values.empty?
|
|
281
|
+
|
|
282
|
+
raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors)
|
|
283
|
+
end
|
|
284
|
+
|
|
273
285
|
def try_map(clients, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
|
274
286
|
return [{}, {}] if clients.empty?
|
|
275
287
|
|
|
@@ -314,7 +326,6 @@ class RedisClient
|
|
|
314
326
|
e
|
|
315
327
|
ensure
|
|
316
328
|
client.read_timeout = regular_timeout
|
|
317
|
-
client&.close
|
|
318
329
|
end
|
|
319
330
|
end
|
|
320
331
|
|
|
@@ -378,7 +389,7 @@ class RedisClient
|
|
|
378
389
|
config_epoch: fields[6],
|
|
379
390
|
link_state: fields[7],
|
|
380
391
|
slots: slots
|
|
381
|
-
)
|
|
392
|
+
).freeze
|
|
382
393
|
end
|
|
383
394
|
end
|
|
384
395
|
|
|
@@ -398,7 +409,7 @@ class RedisClient
|
|
|
398
409
|
role: role,
|
|
399
410
|
primary_id: role == 'master' ? EMPTY_STRING : primary_id,
|
|
400
411
|
slots: role == 'master' ? slots : EMPTY_ARRAY
|
|
401
|
-
)
|
|
412
|
+
).freeze
|
|
402
413
|
end
|
|
403
414
|
end
|
|
404
415
|
end
|
|
@@ -409,7 +420,7 @@ class RedisClient
|
|
|
409
420
|
shard = shard.each_slice(2).to_h if resp2
|
|
410
421
|
nodes = shard.fetch('nodes')
|
|
411
422
|
nodes = nodes.map { |n| n.each_slice(2).to_h } if resp2
|
|
412
|
-
primary_id = nodes.find { |n| n.fetch('role') == 'master' }
|
|
423
|
+
primary_id = nodes.find { |n| n.fetch('role') == 'master' }&.fetch('id') || EMPTY_STRING
|
|
413
424
|
|
|
414
425
|
nodes.each do |node|
|
|
415
426
|
host = pick_shard_host(node)
|
|
@@ -422,7 +433,7 @@ class RedisClient
|
|
|
422
433
|
role: role == 'master' ? role : 'slave',
|
|
423
434
|
primary_id: role == 'master' ? EMPTY_STRING : primary_id,
|
|
424
435
|
slots: role == 'master' ? shard.fetch('slots').each_slice(2).to_a.freeze : EMPTY_ARRAY
|
|
425
|
-
)
|
|
436
|
+
).freeze
|
|
426
437
|
end
|
|
427
438
|
end
|
|
428
439
|
end
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require 'redis_client'
|
|
4
4
|
require 'redis_client/cluster/transaction'
|
|
5
|
+
require 'redis_client/cluster/error_identification'
|
|
5
6
|
|
|
6
7
|
class RedisClient
|
|
7
8
|
class Cluster
|
|
@@ -54,7 +55,7 @@ class RedisClient
|
|
|
54
55
|
rescue ::RedisClient::ConnectionError
|
|
55
56
|
# Deduct the number of retries that happened _inside_ router#handle_redirection from our remaining
|
|
56
57
|
# _external_ retries. Always deduct at least one in case handle_redirection raises without trying the block.
|
|
57
|
-
retry_count -= times_block_executed
|
|
58
|
+
retry_count -= times_block_executed > 1 ? times_block_executed : 1
|
|
58
59
|
raise if retry_count < 0
|
|
59
60
|
|
|
60
61
|
retry
|
|
@@ -23,10 +23,6 @@ class RedisClient
|
|
|
23
23
|
@outer_indices << index
|
|
24
24
|
end
|
|
25
25
|
|
|
26
|
-
def get_inner_index(outer_index)
|
|
27
|
-
@outer_indices&.find_index(outer_index)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
26
|
def get_callee_method(inner_index)
|
|
31
27
|
if @timeouts.is_a?(Array) && !@timeouts[inner_index].nil?
|
|
32
28
|
:blocking_call_v
|
|
@@ -66,6 +62,7 @@ class RedisClient
|
|
|
66
62
|
if result.is_a?(::RedisClient::Error)
|
|
67
63
|
result._set_command(commands[index])
|
|
68
64
|
result._set_config(config)
|
|
65
|
+
result._set_retry_attempt(@retry_attempt)
|
|
69
66
|
|
|
70
67
|
if result.is_a?(::RedisClient::CommandError) && result.message.start_with?('MOVED', 'ASK')
|
|
71
68
|
redirection_indices ||= []
|
|
@@ -61,17 +61,7 @@ class RedisClient
|
|
|
61
61
|
end
|
|
62
62
|
|
|
63
63
|
BUF_SIZE = Integer(ENV.fetch('REDIS_CLIENT_PUBSUB_BUF_SIZE', 1024))
|
|
64
|
-
|
|
65
|
-
RECOVERY_MAX_INTERVAL = Float(ENV.fetch('REDIS_CLIENT_PUBSUB_RECOVERY_MAX_INTERVAL_SEC', 30.0))
|
|
66
|
-
RECOVERY_MAX_ATTEMPTS = Integer(ENV.fetch('REDIS_CLIENT_PUBSUB_RECOVERY_MAX_ATTEMPTS', 10))
|
|
67
|
-
SUBSCRIBE_COMMANDS = %w[subscribe psubscribe ssubscribe].freeze
|
|
68
|
-
UNSUBSCRIBE_TO_SUBSCRIBE = {
|
|
69
|
-
'unsubscribe' => 'subscribe',
|
|
70
|
-
'punsubscribe' => 'psubscribe',
|
|
71
|
-
'sunsubscribe' => 'ssubscribe'
|
|
72
|
-
}.freeze
|
|
73
|
-
private_constant :BUF_SIZE, :RECOVERY_BASE_INTERVAL, :RECOVERY_MAX_INTERVAL, :RECOVERY_MAX_ATTEMPTS,
|
|
74
|
-
:SUBSCRIBE_COMMANDS, :UNSUBSCRIBE_TO_SUBSCRIBE
|
|
64
|
+
private_constant :BUF_SIZE
|
|
75
65
|
|
|
76
66
|
def initialize(router, command_builder)
|
|
77
67
|
@router = router
|
|
@@ -84,14 +74,14 @@ class RedisClient
|
|
|
84
74
|
def call(*args, **kwargs)
|
|
85
75
|
command = @command_builder.generate(args, kwargs)
|
|
86
76
|
_call(command)
|
|
87
|
-
|
|
77
|
+
remember_subscription(command)
|
|
88
78
|
nil
|
|
89
79
|
end
|
|
90
80
|
|
|
91
81
|
def call_v(command)
|
|
92
82
|
command = @command_builder.generate(command)
|
|
93
83
|
_call(command)
|
|
94
|
-
|
|
84
|
+
remember_subscription(command)
|
|
95
85
|
nil
|
|
96
86
|
end
|
|
97
87
|
|
|
@@ -128,35 +118,6 @@ class RedisClient
|
|
|
128
118
|
|
|
129
119
|
private
|
|
130
120
|
|
|
131
|
-
def remember(command)
|
|
132
|
-
cmd = command.first.to_s.downcase
|
|
133
|
-
if SUBSCRIBE_COMMANDS.include?(cmd)
|
|
134
|
-
@commands << command
|
|
135
|
-
elsif (subscribe_cmd = UNSUBSCRIBE_TO_SUBSCRIBE[cmd])
|
|
136
|
-
forget_subscriptions(subscribe_cmd, command[1..])
|
|
137
|
-
else
|
|
138
|
-
@commands << command
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
def forget_subscriptions(subscribe_cmd, channels)
|
|
143
|
-
if channels.nil? || channels.empty?
|
|
144
|
-
@commands.reject! { |c| c.first.to_s.casecmp(subscribe_cmd).zero? }
|
|
145
|
-
else
|
|
146
|
-
@commands.reject! { |c| prune_entry(c, subscribe_cmd, channels) }
|
|
147
|
-
end
|
|
148
|
-
end
|
|
149
|
-
|
|
150
|
-
def prune_entry(entry, subscribe_cmd, channels)
|
|
151
|
-
return false unless entry.first.to_s.casecmp(subscribe_cmd).zero?
|
|
152
|
-
|
|
153
|
-
remaining = entry[1..] - channels
|
|
154
|
-
return true if remaining.empty?
|
|
155
|
-
|
|
156
|
-
entry.replace([entry.first, *remaining])
|
|
157
|
-
false
|
|
158
|
-
end
|
|
159
|
-
|
|
160
121
|
def _call(command) # rubocop:disable Metrics/AbcSize
|
|
161
122
|
if command.first.casecmp('subscribe').zero?
|
|
162
123
|
call_to_single_state(command)
|
|
@@ -227,14 +188,44 @@ class RedisClient
|
|
|
227
188
|
break
|
|
228
189
|
rescue ::RedisClient::ConnectionError, ::RedisClient::Cluster::NodeMightBeDown
|
|
229
190
|
attempt += 1
|
|
230
|
-
raise if attempt >=
|
|
191
|
+
raise if attempt >= 10
|
|
231
192
|
|
|
232
193
|
sleep recovery_interval(attempt)
|
|
233
194
|
end
|
|
234
195
|
end
|
|
235
196
|
|
|
236
197
|
def recovery_interval(attempt)
|
|
237
|
-
[
|
|
198
|
+
[1.0 * (2**(attempt - 1)), 30.0].min
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def remember_subscription(command) # rubocop:disable Metrics/AbcSize
|
|
202
|
+
if command.first.casecmp('subscribe').zero?
|
|
203
|
+
@commands << command
|
|
204
|
+
elsif command.first.casecmp('psubscribe').zero?
|
|
205
|
+
@commands << command
|
|
206
|
+
elsif command.first.casecmp('ssubscribe').zero?
|
|
207
|
+
@commands << command
|
|
208
|
+
elsif command.first.casecmp('unsubscribe').zero?
|
|
209
|
+
forget_subscriptions('subscribe', command[1, command.size])
|
|
210
|
+
elsif command.first.casecmp('punsubscribe').zero?
|
|
211
|
+
forget_subscriptions('psubscribe', command[1, command.size])
|
|
212
|
+
elsif command.first.casecmp('sunsubscribe').zero?
|
|
213
|
+
forget_subscriptions('ssubscribe', command[1, command.size])
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def forget_subscriptions(subscribe_cmd, channels)
|
|
218
|
+
@commands.map! do |command|
|
|
219
|
+
next command unless command.first.casecmp(subscribe_cmd).zero?
|
|
220
|
+
next if channels.nil? || channels.empty?
|
|
221
|
+
|
|
222
|
+
remaining = command[1, command.size] - channels
|
|
223
|
+
next if remaining.empty?
|
|
224
|
+
|
|
225
|
+
[command.first, *remaining]
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
@commands.compact!
|
|
238
229
|
end
|
|
239
230
|
end
|
|
240
231
|
end
|
|
@@ -17,38 +17,39 @@ class RedisClient
|
|
|
17
17
|
class Router
|
|
18
18
|
ZERO_CURSOR_FOR_SCAN = '0'
|
|
19
19
|
TSF = ->(f, x) { f.nil? ? x : f.call(x) }.curry
|
|
20
|
+
RoutingAction = Struct.new('RedisCommandRoutingAction', :method_name, :reply_transformer, keyword_init: true)
|
|
21
|
+
PICK_FIRST = ->(reply) { reply.first } # rubocop:disable Style/SymbolProc
|
|
22
|
+
FLATTEN_STRINGS = ->(reply) { reply.flatten.sort_by(&:to_s) }
|
|
23
|
+
SUM_NUM = ->(reply) { reply.select { |e| e.is_a?(Integer) }.sum }
|
|
24
|
+
SORT_NUMBERS = ->(reply) { reply.sort_by(&:to_i) }
|
|
25
|
+
if Object.const_defined?(:Ractor, false) && Ractor.respond_to?(:make_shareable)
|
|
26
|
+
Ractor.make_shareable(PICK_FIRST)
|
|
27
|
+
Ractor.make_shareable(FLATTEN_STRINGS)
|
|
28
|
+
Ractor.make_shareable(SUM_NUM)
|
|
29
|
+
Ractor.make_shareable(SORT_NUMBERS)
|
|
30
|
+
end
|
|
20
31
|
DEDICATED_ACTIONS = lambda do # rubocop:disable Metrics/BlockLength
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Ractor.make_shareable(pick_first)
|
|
28
|
-
Ractor.make_shareable(flatten_strings)
|
|
29
|
-
Ractor.make_shareable(sum_num)
|
|
30
|
-
Ractor.make_shareable(sort_numbers)
|
|
31
|
-
end
|
|
32
|
-
multiple_key_action = action.new(method_name: :send_multiple_keys_command)
|
|
33
|
-
all_node_first_action = action.new(method_name: :send_command_to_all_nodes, reply_transformer: pick_first)
|
|
34
|
-
primary_first_action = action.new(method_name: :send_command_to_primaries, reply_transformer: pick_first)
|
|
35
|
-
not_supported_action = action.new(method_name: :fail_not_supported_command)
|
|
36
|
-
keyless_action = action.new(method_name: :fail_keyless_command)
|
|
32
|
+
multiple_key_action = RoutingAction.new(method_name: :send_multiple_keys_command)
|
|
33
|
+
all_node_first_action = RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: PICK_FIRST)
|
|
34
|
+
primary_first_action = RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: PICK_FIRST)
|
|
35
|
+
not_supported_action = RoutingAction.new(method_name: :fail_not_supported_command)
|
|
36
|
+
keyless_action = RoutingAction.new(method_name: :fail_keyless_command)
|
|
37
|
+
single_node_action = RoutingAction.new(method_name: :assign_node_and_send_command)
|
|
37
38
|
{
|
|
38
|
-
'ping' =>
|
|
39
|
-
'wait' =>
|
|
40
|
-
'keys' =>
|
|
41
|
-
'dbsize' =>
|
|
42
|
-
'scan' =>
|
|
43
|
-
'lastsave' =>
|
|
44
|
-
'role' =>
|
|
45
|
-
'config' =>
|
|
46
|
-
'client' =>
|
|
47
|
-
'cluster' =>
|
|
48
|
-
'memory' =>
|
|
49
|
-
'script' =>
|
|
50
|
-
'pubsub' =>
|
|
51
|
-
'watch' =>
|
|
39
|
+
'ping' => RoutingAction.new(method_name: :send_ping_command, reply_transformer: PICK_FIRST),
|
|
40
|
+
'wait' => RoutingAction.new(method_name: :send_wait_command),
|
|
41
|
+
'keys' => RoutingAction.new(method_name: :send_command_to_replicas, reply_transformer: FLATTEN_STRINGS),
|
|
42
|
+
'dbsize' => RoutingAction.new(method_name: :send_command_to_replicas, reply_transformer: SUM_NUM),
|
|
43
|
+
'scan' => RoutingAction.new(method_name: :send_scan_command),
|
|
44
|
+
'lastsave' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: SORT_NUMBERS),
|
|
45
|
+
'role' => RoutingAction.new(method_name: :send_command_to_all_nodes),
|
|
46
|
+
'config' => RoutingAction.new(method_name: :send_config_command),
|
|
47
|
+
'client' => RoutingAction.new(method_name: :send_client_command),
|
|
48
|
+
'cluster' => RoutingAction.new(method_name: :send_cluster_command),
|
|
49
|
+
'memory' => RoutingAction.new(method_name: :send_memory_command),
|
|
50
|
+
'script' => RoutingAction.new(method_name: :send_script_command),
|
|
51
|
+
'pubsub' => RoutingAction.new(method_name: :send_pubsub_command),
|
|
52
|
+
'watch' => RoutingAction.new(method_name: :send_watch_command),
|
|
52
53
|
'mget' => multiple_key_action,
|
|
53
54
|
'mset' => multiple_key_action,
|
|
54
55
|
'del' => multiple_key_action,
|
|
@@ -61,6 +62,10 @@ class RedisClient
|
|
|
61
62
|
'select' => all_node_first_action,
|
|
62
63
|
'flushall' => primary_first_action,
|
|
63
64
|
'flushdb' => primary_first_action,
|
|
65
|
+
# The redis 7.0 tags RANDOMKEY with `request_policy:all_shards` but without any response policy.
|
|
66
|
+
# It was corrected to `response_policy:special` in the redis 7.2.
|
|
67
|
+
# This entry keeps the historical single node routing for the redis 7.0.
|
|
68
|
+
'randomkey' => single_node_action,
|
|
64
69
|
'readonly' => not_supported_action,
|
|
65
70
|
'readwrite' => not_supported_action,
|
|
66
71
|
'shutdown' => not_supported_action,
|
|
@@ -74,7 +79,33 @@ class RedisClient
|
|
|
74
79
|
end
|
|
75
80
|
end.call.freeze
|
|
76
81
|
|
|
77
|
-
|
|
82
|
+
# The routing which the command tips of the COMMAND command reply instruct.
|
|
83
|
+
# The `multi_shard` and the `special` request policies are out of scope.
|
|
84
|
+
# The `special` response policy is also out of scope because the aggregation is undefined,
|
|
85
|
+
# and the fan-out would change the return value of commands such as INFO.
|
|
86
|
+
# The `agg_min`, `agg_max` and `agg_logical_*` response policies are out of scope too:
|
|
87
|
+
# the only reachable command with them today is WAITAOF, whose reply is an array,
|
|
88
|
+
# and the aggregation of array replies is undefined. Such a command falls back to
|
|
89
|
+
# the single node routing until a command with a settled semantics appears.
|
|
90
|
+
# The entries of the DEDICATED_ACTIONS take precedence over these to keep the existing behavior.
|
|
91
|
+
# @see https://redis.io/docs/latest/develop/reference/command-tips/
|
|
92
|
+
POLICY_ACTIONS = {
|
|
93
|
+
'all_shards' => {
|
|
94
|
+
nil => RoutingAction.new(method_name: :send_command_to_primaries).freeze,
|
|
95
|
+
'all_succeeded' => RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: PICK_FIRST).freeze,
|
|
96
|
+
'one_succeeded' => RoutingAction.new(method_name: :send_command_to_primaries_leniently, reply_transformer: PICK_FIRST).freeze,
|
|
97
|
+
'agg_sum' => RoutingAction.new(method_name: :send_command_to_primaries, reply_transformer: SUM_NUM).freeze
|
|
98
|
+
}.freeze,
|
|
99
|
+
'all_nodes' => {
|
|
100
|
+
nil => RoutingAction.new(method_name: :send_command_to_all_nodes).freeze,
|
|
101
|
+
'all_succeeded' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: PICK_FIRST).freeze,
|
|
102
|
+
'one_succeeded' => RoutingAction.new(method_name: :send_command_to_all_nodes_leniently, reply_transformer: PICK_FIRST).freeze,
|
|
103
|
+
'agg_sum' => RoutingAction.new(method_name: :send_command_to_all_nodes, reply_transformer: SUM_NUM).freeze
|
|
104
|
+
}.freeze
|
|
105
|
+
}.freeze
|
|
106
|
+
|
|
107
|
+
private_constant :ZERO_CURSOR_FOR_SCAN, :TSF, :RoutingAction, :PICK_FIRST, :FLATTEN_STRINGS,
|
|
108
|
+
:SUM_NUM, :SORT_NUMBERS, :DEDICATED_ACTIONS, :POLICY_ACTIONS
|
|
78
109
|
|
|
79
110
|
attr_reader :config
|
|
80
111
|
|
|
@@ -94,7 +125,12 @@ class RedisClient
|
|
|
94
125
|
|
|
95
126
|
def send_command(method, command, *args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
96
127
|
action = DEDICATED_ACTIONS[command.first]
|
|
97
|
-
|
|
128
|
+
if action.nil?
|
|
129
|
+
cmd_spec = @command.get_spec(command)
|
|
130
|
+
action = find_policy_action(cmd_spec)
|
|
131
|
+
return assign_node_and_send_command(method, command, args, cmd_spec: cmd_spec, &block) if action.nil?
|
|
132
|
+
end
|
|
133
|
+
|
|
98
134
|
return send(action.method_name, method, command, args, &block) if action.reply_transformer.nil?
|
|
99
135
|
|
|
100
136
|
reply = send(action.method_name, method, command, args)
|
|
@@ -124,8 +160,8 @@ class RedisClient
|
|
|
124
160
|
end
|
|
125
161
|
|
|
126
162
|
# @see https://redis.io/docs/reference/cluster-spec/#redirection-and-resharding Redirection and resharding
|
|
127
|
-
def assign_node_and_send_command(method, command, args, retry_count: 3, &block)
|
|
128
|
-
node = assign_node(command)
|
|
163
|
+
def assign_node_and_send_command(method, command, args, retry_count: 3, cmd_spec: nil, &block)
|
|
164
|
+
node = assign_node(command, cmd_spec: cmd_spec)
|
|
129
165
|
send_command_to_node(node, method, command, args, retry_count: retry_count, &block)
|
|
130
166
|
end
|
|
131
167
|
|
|
@@ -167,19 +203,20 @@ class RedisClient
|
|
|
167
203
|
raise unless ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(e, node)
|
|
168
204
|
|
|
169
205
|
renew_cluster_state
|
|
170
|
-
|
|
206
|
+
raise if command.nil? || command.empty?
|
|
171
207
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
208
|
+
retry_count -= 1
|
|
209
|
+
raise if retry_count < 0
|
|
210
|
+
|
|
211
|
+
# Find the node to use for this command - if this fails for some reason, though, re-use
|
|
212
|
+
# the old node.
|
|
213
|
+
begin
|
|
214
|
+
node = find_node(find_node_key(command))
|
|
215
|
+
rescue StandardError
|
|
216
|
+
raise e
|
|
180
217
|
end
|
|
181
218
|
|
|
182
|
-
|
|
219
|
+
retry
|
|
183
220
|
end
|
|
184
221
|
|
|
185
222
|
def scan(command, seed: nil) # rubocop:disable Metrics/AbcSize
|
|
@@ -217,9 +254,9 @@ class RedisClient
|
|
|
217
254
|
end
|
|
218
255
|
end
|
|
219
256
|
|
|
220
|
-
def assign_node(command)
|
|
257
|
+
def assign_node(command, cmd_spec: nil)
|
|
221
258
|
handle_node_reload_error do
|
|
222
|
-
node_key = find_node_key(command)
|
|
259
|
+
node_key = find_node_key(command, cmd_spec: cmd_spec)
|
|
223
260
|
@node.find_by(node_key)
|
|
224
261
|
end
|
|
225
262
|
end
|
|
@@ -227,7 +264,7 @@ class RedisClient
|
|
|
227
264
|
def find_node_key_by_key(key, seed: nil, primary: false)
|
|
228
265
|
if key && !key.empty?
|
|
229
266
|
slot = ::RedisClient::Cluster::KeySlotConverter.convert(key)
|
|
230
|
-
node_key = primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot)
|
|
267
|
+
node_key = primary ? @node.find_node_key_of_primary(slot) : @node.find_node_key_of_replica(slot, seed: seed)
|
|
231
268
|
if node_key.nil?
|
|
232
269
|
renew_cluster_state
|
|
233
270
|
raise ::RedisClient::Cluster::NodeMightBeDown.new.with_config(@config)
|
|
@@ -245,8 +282,8 @@ class RedisClient
|
|
|
245
282
|
end
|
|
246
283
|
end
|
|
247
284
|
|
|
248
|
-
def find_node_key(command, seed: nil)
|
|
249
|
-
cmd_spec = @command.get_spec(command.
|
|
285
|
+
def find_node_key(command, seed: nil, cmd_spec: nil)
|
|
286
|
+
cmd_spec = @command.get_spec(command) if cmd_spec.nil?
|
|
250
287
|
find_node_key_by_key(
|
|
251
288
|
cmd_spec&.extract_first_key(command),
|
|
252
289
|
seed: seed,
|
|
@@ -255,14 +292,14 @@ class RedisClient
|
|
|
255
292
|
end
|
|
256
293
|
|
|
257
294
|
def find_primary_node_key(command)
|
|
258
|
-
key = @command.get_spec(command
|
|
295
|
+
key = @command.get_spec(command)&.extract_first_key(command)
|
|
259
296
|
return unless key&.size&.> 0
|
|
260
297
|
|
|
261
298
|
find_node_key_by_key(key, primary: true)
|
|
262
299
|
end
|
|
263
300
|
|
|
264
301
|
def find_slot(command)
|
|
265
|
-
find_slot_by_key(@command.get_spec(command
|
|
302
|
+
find_slot_by_key(@command.get_spec(command)&.extract_first_key(command))
|
|
266
303
|
end
|
|
267
304
|
|
|
268
305
|
def find_slot_by_key(key)
|
|
@@ -307,6 +344,15 @@ class RedisClient
|
|
|
307
344
|
|
|
308
345
|
private
|
|
309
346
|
|
|
347
|
+
def find_policy_action(cmd_spec)
|
|
348
|
+
return if cmd_spec.nil?
|
|
349
|
+
|
|
350
|
+
request_policy = cmd_spec.request_policy
|
|
351
|
+
return if request_policy.nil?
|
|
352
|
+
|
|
353
|
+
POLICY_ACTIONS.dig(request_policy, cmd_spec.response_policy)
|
|
354
|
+
end
|
|
355
|
+
|
|
310
356
|
def send_command_to_all_nodes(method, command, args, &block)
|
|
311
357
|
@node.call_all(method, command, args, &block)
|
|
312
358
|
end
|
|
@@ -315,6 +361,14 @@ class RedisClient
|
|
|
315
361
|
@node.call_primaries(method, command, args, &block)
|
|
316
362
|
end
|
|
317
363
|
|
|
364
|
+
def send_command_to_all_nodes_leniently(method, command, args, &block)
|
|
365
|
+
@node.call_all_leniently(method, command, args, &block)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
def send_command_to_primaries_leniently(method, command, args, &block)
|
|
369
|
+
@node.call_primaries_leniently(method, command, args, &block)
|
|
370
|
+
end
|
|
371
|
+
|
|
318
372
|
def send_command_to_replicas(method, command, args, &block)
|
|
319
373
|
@node.call_replicas(method, command, args, &block)
|
|
320
374
|
end
|
|
@@ -171,7 +171,7 @@ class RedisClient
|
|
|
171
171
|
send_transaction(node, redirect: redirect - 1)
|
|
172
172
|
elsif err.message.start_with?('ASK')
|
|
173
173
|
node = @router.assign_asking_node(err.message)
|
|
174
|
-
try_asking(node) ? send_transaction(node, redirect: redirect - 1) : err
|
|
174
|
+
try_asking(node) ? send_transaction(node, redirect: redirect - 1) : raise(err)
|
|
175
175
|
elsif err.message.start_with?('CLUSTERDOWN')
|
|
176
176
|
@router.renew_cluster_state if @watching_slot.nil?
|
|
177
177
|
raise err
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: redis-cluster-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.16.
|
|
4
|
+
version: 0.16.6
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Taishi Kasuga
|
|
@@ -74,7 +74,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
75
|
version: '0'
|
|
76
76
|
requirements: []
|
|
77
|
-
rubygems_version: 4.0.
|
|
77
|
+
rubygems_version: 4.0.16
|
|
78
78
|
specification_version: 4
|
|
79
79
|
summary: Redis cluster-aware client for Ruby
|
|
80
80
|
test_files: []
|