redis-cluster-client 0.10.0 → 0.11.1
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 +2 -0
- data/lib/redis_client/cluster/errors.rb +3 -1
- data/lib/redis_client/cluster/key_slot_converter.rb +3 -1
- data/lib/redis_client/cluster/node/base_topology.rb +2 -0
- data/lib/redis_client/cluster/node/latency_replica.rb +2 -0
- data/lib/redis_client/cluster/node.rb +23 -6
- data/lib/redis_client/cluster/node_key.rb +2 -0
- data/lib/redis_client/cluster/normalized_cmd_name.rb +2 -0
- data/lib/redis_client/cluster/optimistic_locking.rb +7 -1
- data/lib/redis_client/cluster/pipeline.rb +45 -11
- data/lib/redis_client/cluster/pub_sub.rb +43 -24
- data/lib/redis_client/cluster/router.rb +55 -36
- data/lib/redis_client/cluster/transaction.rb +10 -1
- data/lib/redis_client/cluster.rb +2 -0
- data/lib/redis_client/cluster_config.rb +10 -2
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: cd66efdf343224572aeffdbfdd4cb96c8189bd45183c0a3934e6dd1964a35655
|
4
|
+
data.tar.gz: 66805c538066aabab413b767f0b9e8756c1eff8920f410a665939f18d69c0621
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d325c2955718451c15084b58b0231d521065ebe1b8b549f79106f26132b50eb77ac90e6b0dcfb11868eb80b2d37a742f741b21d963c6ff92faacf4d88bc64830
|
7
|
+
data.tar.gz: 7e6c92442c2852b0533c19b4e60ff8e8e6532d55cbeebc9424e1b62081c6eb21af6a5717ae04fbb683083c271d3008934d0e17becc4b7abf232c005304c2b713
|
@@ -6,6 +6,8 @@ class RedisClient
|
|
6
6
|
class Cluster
|
7
7
|
ERR_ARG_NORMALIZATION = ->(arg) { Array[arg].flatten.reject { |e| e.nil? || (e.respond_to?(:empty?) && e.empty?) } }
|
8
8
|
|
9
|
+
private_constant :ERR_ARG_NORMALIZATION
|
10
|
+
|
9
11
|
class InitialSetupError < ::RedisClient::Error
|
10
12
|
def initialize(errors)
|
11
13
|
msg = ERR_ARG_NORMALIZATION.call(errors).map(&:message).uniq.join(',')
|
@@ -30,7 +32,7 @@ class RedisClient
|
|
30
32
|
def initialize(errors)
|
31
33
|
@errors = {}
|
32
34
|
if !errors.is_a?(Hash) || errors.empty?
|
33
|
-
super(
|
35
|
+
super(errors.to_s)
|
34
36
|
return
|
35
37
|
end
|
36
38
|
|
@@ -3,6 +3,7 @@
|
|
3
3
|
class RedisClient
|
4
4
|
class Cluster
|
5
5
|
module KeySlotConverter
|
6
|
+
HASH_SLOTS = 16_384
|
6
7
|
EMPTY_STRING = ''
|
7
8
|
LEFT_BRACKET = '{'
|
8
9
|
RIGHT_BRACKET = '}'
|
@@ -41,7 +42,8 @@ class RedisClient
|
|
41
42
|
0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0
|
42
43
|
].freeze
|
43
44
|
|
44
|
-
HASH_SLOTS
|
45
|
+
private_constant :HASH_SLOTS, :EMPTY_STRING,
|
46
|
+
:LEFT_BRACKET, :RIGHT_BRACKET, :XMODEM_CRC16_LOOKUP
|
45
47
|
|
46
48
|
module_function
|
47
49
|
|
@@ -8,6 +8,8 @@ class RedisClient
|
|
8
8
|
EMPTY_HASH = {}.freeze
|
9
9
|
EMPTY_ARRAY = [].freeze
|
10
10
|
|
11
|
+
private_constant :IGNORE_GENERIC_CONFIG_KEYS, :EMPTY_HASH, :EMPTY_ARRAY
|
12
|
+
|
11
13
|
attr_reader :clients, :primary_clients, :replica_clients
|
12
14
|
|
13
15
|
def initialize(pool, concurrent_worker, **kwargs)
|
@@ -9,6 +9,8 @@ class RedisClient
|
|
9
9
|
DUMMY_LATENCY_MSEC = 100 * 1000 * 1000
|
10
10
|
MEASURE_ATTEMPT_COUNT = 10
|
11
11
|
|
12
|
+
private_constant :DUMMY_LATENCY_MSEC, :MEASURE_ATTEMPT_COUNT
|
13
|
+
|
12
14
|
def clients_for_scanning(seed: nil) # rubocop:disable Lint/UnusedMethodArgument
|
13
15
|
@clients_for_scanning
|
14
16
|
end
|
@@ -13,9 +13,6 @@ class RedisClient
|
|
13
13
|
class Node
|
14
14
|
include Enumerable
|
15
15
|
|
16
|
-
# It affects to strike a balance between load and stability in initialization or changed states.
|
17
|
-
MAX_STARTUP_SAMPLE = Integer(ENV.fetch('REDIS_CLIENT_MAX_STARTUP_SAMPLE', 3))
|
18
|
-
|
19
16
|
# less memory consumption, but slow
|
20
17
|
USE_CHAR_ARRAY_SLOT = Integer(ENV.fetch('REDIS_CLIENT_USE_CHAR_ARRAY_SLOT', 1)) == 1
|
21
18
|
|
@@ -26,6 +23,10 @@ class RedisClient
|
|
26
23
|
ROLE_FLAGS = %w[master slave].freeze
|
27
24
|
EMPTY_ARRAY = [].freeze
|
28
25
|
EMPTY_HASH = {}.freeze
|
26
|
+
STATE_REFRESH_INTERVAL = (3..10).freeze
|
27
|
+
|
28
|
+
private_constant :USE_CHAR_ARRAY_SLOT, :SLOT_SIZE, :MIN_SLOT, :MAX_SLOT,
|
29
|
+
:DEAD_FLAGS, :ROLE_FLAGS, :EMPTY_ARRAY, :EMPTY_HASH
|
29
30
|
|
30
31
|
ReloadNeeded = Class.new(::RedisClient::Error)
|
31
32
|
|
@@ -48,6 +49,8 @@ class RedisClient
|
|
48
49
|
BASE = ''
|
49
50
|
PADDING = '0'
|
50
51
|
|
52
|
+
private_constant :BASE, :PADDING
|
53
|
+
|
51
54
|
def initialize(size, elements)
|
52
55
|
@elements = elements
|
53
56
|
@string = String.new(BASE, encoding: Encoding::BINARY, capacity: size)
|
@@ -101,6 +104,8 @@ class RedisClient
|
|
101
104
|
@config = config
|
102
105
|
@mutex = Mutex.new
|
103
106
|
@last_reloaded_at = nil
|
107
|
+
@reload_times = 0
|
108
|
+
@random = Random.new
|
104
109
|
end
|
105
110
|
|
106
111
|
def inspect
|
@@ -197,7 +202,7 @@ class RedisClient
|
|
197
202
|
|
198
203
|
def reload!
|
199
204
|
with_reload_lock do
|
200
|
-
with_startup_clients(
|
205
|
+
with_startup_clients(@config.max_startup_sample) do |startup_clients|
|
201
206
|
@node_info = refetch_node_info_list(startup_clients)
|
202
207
|
@node_configs = @node_info.to_h do |node_info|
|
203
208
|
[node_info.node_key, @config.client_config_for_node(node_info.node_key)]
|
@@ -417,15 +422,27 @@ class RedisClient
|
|
417
422
|
# performed the reload.
|
418
423
|
# Probably in the future we should add a circuit breaker to #reload itself, and stop trying if the cluster is
|
419
424
|
# obviously not working.
|
420
|
-
wait_start =
|
425
|
+
wait_start = obtain_current_time
|
421
426
|
@mutex.synchronize do
|
422
427
|
return if @last_reloaded_at && @last_reloaded_at > wait_start
|
423
428
|
|
429
|
+
if @last_reloaded_at && @reload_times > 1
|
430
|
+
# Mitigate load of servers by naive logic. Don't sleep with exponential backoff.
|
431
|
+
now = obtain_current_time
|
432
|
+
elapsed = @last_reloaded_at + @random.rand(STATE_REFRESH_INTERVAL) * 1_000_000
|
433
|
+
return if now < elapsed
|
434
|
+
end
|
435
|
+
|
424
436
|
r = yield
|
425
|
-
@last_reloaded_at =
|
437
|
+
@last_reloaded_at = obtain_current_time
|
438
|
+
@reload_times += 1
|
426
439
|
r
|
427
440
|
end
|
428
441
|
end
|
442
|
+
|
443
|
+
def obtain_current_time
|
444
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
445
|
+
end
|
429
446
|
end
|
430
447
|
end
|
431
448
|
end
|
@@ -11,7 +11,7 @@ class RedisClient
|
|
11
11
|
@asking = false
|
12
12
|
end
|
13
13
|
|
14
|
-
def watch(keys)
|
14
|
+
def watch(keys) # rubocop:disable Metrics/AbcSize
|
15
15
|
slot = find_slot(keys)
|
16
16
|
raise ::RedisClient::Cluster::Transaction::ConsistencyError, "unsafe watch: #{keys.join(' ')}" if slot.nil?
|
17
17
|
|
@@ -32,7 +32,13 @@ class RedisClient
|
|
32
32
|
c.call('UNWATCH')
|
33
33
|
raise
|
34
34
|
end
|
35
|
+
rescue ::RedisClient::CommandError => e
|
36
|
+
@router.renew_cluster_state if e.message.start_with?('CLUSTERDOWN Hash slot not served')
|
37
|
+
raise
|
35
38
|
end
|
39
|
+
rescue ::RedisClient::ConnectionError
|
40
|
+
@router.renew_cluster_state
|
41
|
+
raise
|
36
42
|
end
|
37
43
|
end
|
38
44
|
end
|
@@ -55,32 +55,48 @@ class RedisClient
|
|
55
55
|
results = Array.new(commands.size)
|
56
56
|
@pending_reads += size
|
57
57
|
write_multi(commands)
|
58
|
+
redirection_indices = stale_cluster_state = first_exception = nil
|
58
59
|
|
59
|
-
redirection_indices = nil
|
60
|
-
first_exception = nil
|
61
60
|
size.times do |index|
|
62
61
|
timeout = timeouts && timeouts[index]
|
63
|
-
result = read(timeout)
|
62
|
+
result = read(connection_timeout(timeout))
|
64
63
|
@pending_reads -= 1
|
64
|
+
|
65
65
|
if result.is_a?(::RedisClient::Error)
|
66
66
|
result._set_command(commands[index])
|
67
|
+
result._set_config(config)
|
68
|
+
|
67
69
|
if result.is_a?(::RedisClient::CommandError) && result.message.start_with?('MOVED', 'ASK')
|
68
70
|
redirection_indices ||= []
|
69
71
|
redirection_indices << index
|
70
72
|
elsif exception
|
71
73
|
first_exception ||= result
|
72
74
|
end
|
75
|
+
|
76
|
+
stale_cluster_state = true if result.message.start_with?('CLUSTERDOWN Hash slot not served')
|
73
77
|
end
|
78
|
+
|
74
79
|
results[index] = result
|
75
80
|
end
|
76
81
|
|
77
|
-
|
78
|
-
|
82
|
+
if redirection_indices
|
83
|
+
err = ::RedisClient::Cluster::Pipeline::RedirectionNeeded.new
|
84
|
+
err.replies = results
|
85
|
+
err.indices = redirection_indices
|
86
|
+
err.first_exception = first_exception
|
87
|
+
raise err
|
88
|
+
end
|
89
|
+
|
90
|
+
if stale_cluster_state
|
91
|
+
err = ::RedisClient::Cluster::Pipeline::StaleClusterState.new
|
92
|
+
err.replies = results
|
93
|
+
err.first_exception = first_exception
|
94
|
+
raise err
|
95
|
+
end
|
96
|
+
|
97
|
+
raise first_exception if first_exception
|
79
98
|
|
80
|
-
|
81
|
-
err.replies = results
|
82
|
-
err.indices = redirection_indices
|
83
|
-
raise err
|
99
|
+
results
|
84
100
|
end
|
85
101
|
end
|
86
102
|
|
@@ -94,8 +110,12 @@ class RedisClient
|
|
94
110
|
|
95
111
|
ReplySizeError = Class.new(::RedisClient::Error)
|
96
112
|
|
113
|
+
class StaleClusterState < ::RedisClient::Error
|
114
|
+
attr_accessor :replies, :first_exception
|
115
|
+
end
|
116
|
+
|
97
117
|
class RedirectionNeeded < ::RedisClient::Error
|
98
|
-
attr_accessor :replies, :indices
|
118
|
+
attr_accessor :replies, :indices, :first_exception
|
99
119
|
end
|
100
120
|
|
101
121
|
def initialize(router, command_builder, concurrent_worker, exception:, seed: Random.new_seed)
|
@@ -162,14 +182,18 @@ class RedisClient
|
|
162
182
|
end
|
163
183
|
end
|
164
184
|
|
165
|
-
all_replies = errors = required_redirections = nil
|
185
|
+
all_replies = errors = required_redirections = cluster_state_errors = nil
|
166
186
|
|
167
187
|
work_group.each do |node_key, v|
|
168
188
|
case v
|
169
189
|
when ::RedisClient::Cluster::Pipeline::RedirectionNeeded
|
170
190
|
required_redirections ||= {}
|
171
191
|
required_redirections[node_key] = v
|
192
|
+
when ::RedisClient::Cluster::Pipeline::StaleClusterState
|
193
|
+
cluster_state_errors ||= {}
|
194
|
+
cluster_state_errors[node_key] = v
|
172
195
|
when StandardError
|
196
|
+
cluster_state_errors ||= {} if v.is_a?(::RedisClient::ConnectionError)
|
173
197
|
errors ||= {}
|
174
198
|
errors[node_key] = v
|
175
199
|
else
|
@@ -179,15 +203,25 @@ class RedisClient
|
|
179
203
|
end
|
180
204
|
|
181
205
|
work_group.close
|
206
|
+
@router.renew_cluster_state if cluster_state_errors
|
182
207
|
raise ::RedisClient::Cluster::ErrorCollection, errors unless errors.nil?
|
183
208
|
|
184
209
|
required_redirections&.each do |node_key, v|
|
210
|
+
raise v.first_exception if v.first_exception
|
211
|
+
|
185
212
|
all_replies ||= Array.new(@size)
|
186
213
|
pipeline = @pipelines[node_key]
|
187
214
|
v.indices.each { |i| v.replies[i] = handle_redirection(v.replies[i], pipeline, i) }
|
188
215
|
pipeline.outer_indices.each_with_index { |outer, inner| all_replies[outer] = v.replies[inner] }
|
189
216
|
end
|
190
217
|
|
218
|
+
cluster_state_errors&.each do |node_key, v|
|
219
|
+
raise v.first_exception if v.first_exception
|
220
|
+
|
221
|
+
all_replies ||= Array.new(@size)
|
222
|
+
@pipelines[node_key].outer_indices.each_with_index { |outer, inner| all_replies[outer] = v.replies[inner] }
|
223
|
+
end
|
224
|
+
|
191
225
|
all_replies
|
192
226
|
end
|
193
227
|
|
@@ -24,6 +24,8 @@ class RedisClient
|
|
24
24
|
def close
|
25
25
|
@worker.exit if @worker&.alive?
|
26
26
|
@client.close
|
27
|
+
rescue ::RedisClient::ConnectionError
|
28
|
+
# ignore
|
27
29
|
end
|
28
30
|
|
29
31
|
private
|
@@ -44,32 +46,40 @@ class RedisClient
|
|
44
46
|
|
45
47
|
BUF_SIZE = Integer(ENV.fetch('REDIS_CLIENT_PUBSUB_BUF_SIZE', 1024))
|
46
48
|
|
49
|
+
private_constant :BUF_SIZE
|
50
|
+
|
47
51
|
def initialize(router, command_builder)
|
48
52
|
@router = router
|
49
53
|
@command_builder = command_builder
|
50
54
|
@queue = SizedQueue.new(BUF_SIZE)
|
51
55
|
@state_dict = {}
|
56
|
+
@commands = []
|
52
57
|
end
|
53
58
|
|
54
59
|
def call(*args, **kwargs)
|
55
|
-
|
60
|
+
command = @command_builder.generate(args, kwargs)
|
61
|
+
_call(command)
|
62
|
+
@commands << command
|
56
63
|
nil
|
57
64
|
end
|
58
65
|
|
59
66
|
def call_v(command)
|
60
|
-
|
67
|
+
command = @command_builder.generate(command)
|
68
|
+
_call(command)
|
69
|
+
@commands << command
|
61
70
|
nil
|
62
71
|
end
|
63
72
|
|
64
73
|
def close
|
65
74
|
@state_dict.each_value(&:close)
|
66
75
|
@state_dict.clear
|
76
|
+
@commands.clear
|
67
77
|
@queue.clear
|
68
78
|
@queue.close
|
69
79
|
nil
|
70
80
|
end
|
71
81
|
|
72
|
-
def next_event(timeout = nil)
|
82
|
+
def next_event(timeout = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
73
83
|
@state_dict.each_value(&:ensure_worker)
|
74
84
|
max_duration = calc_max_duration(timeout)
|
75
85
|
starting = obtain_current_time
|
@@ -78,6 +88,13 @@ class RedisClient
|
|
78
88
|
break if max_duration > 0 && obtain_current_time - starting > max_duration
|
79
89
|
|
80
90
|
case event = @queue.pop(true)
|
91
|
+
when ::RedisClient::CommandError
|
92
|
+
if event.message.start_with?('MOVED', 'CLUSTERDOWN Hash slot not served')
|
93
|
+
@router.renew_cluster_state
|
94
|
+
break start_over
|
95
|
+
end
|
96
|
+
|
97
|
+
raise event
|
81
98
|
when StandardError then raise event
|
82
99
|
when Array then break event
|
83
100
|
end
|
@@ -97,13 +114,26 @@ class RedisClient
|
|
97
114
|
end
|
98
115
|
end
|
99
116
|
|
100
|
-
def call_to_single_state(command)
|
117
|
+
def call_to_single_state(command, retry_count: 1)
|
101
118
|
node_key = @router.find_node_key(command)
|
102
|
-
|
119
|
+
@state_dict[node_key] ||= State.new(@router.find_node(node_key).pubsub, @queue)
|
120
|
+
@state_dict[node_key].call(command)
|
121
|
+
rescue ::RedisClient::ConnectionError
|
122
|
+
@state_dict[node_key].close
|
123
|
+
@state_dict.delete(node_key)
|
124
|
+
@router.renew_cluster_state
|
125
|
+
retry_count -= 1
|
126
|
+
retry_count >= 0 ? retry : raise
|
103
127
|
end
|
104
128
|
|
105
129
|
def call_to_all_states(command)
|
106
|
-
@state_dict.
|
130
|
+
@state_dict.each do |node_key, state|
|
131
|
+
state.call(command)
|
132
|
+
rescue ::RedisClient::ConnectionError
|
133
|
+
@state_dict[node_key].close
|
134
|
+
@state_dict.delete(node_key)
|
135
|
+
@router.renew_cluster_state
|
136
|
+
end
|
107
137
|
end
|
108
138
|
|
109
139
|
def call_for_sharded_states(command)
|
@@ -114,24 +144,6 @@ class RedisClient
|
|
114
144
|
end
|
115
145
|
end
|
116
146
|
|
117
|
-
def try_call(node_key, command, retry_count: 1)
|
118
|
-
add_state(node_key).call(command)
|
119
|
-
rescue ::RedisClient::CommandError => e
|
120
|
-
raise if !e.message.start_with?('MOVED') || retry_count <= 0
|
121
|
-
|
122
|
-
# for sharded pub/sub
|
123
|
-
node_key = e.message.split[2]
|
124
|
-
retry_count -= 1
|
125
|
-
retry
|
126
|
-
end
|
127
|
-
|
128
|
-
def add_state(node_key)
|
129
|
-
return @state_dict[node_key] if @state_dict.key?(node_key)
|
130
|
-
|
131
|
-
state = State.new(@router.find_node(node_key).pubsub, @queue)
|
132
|
-
@state_dict[node_key] = state
|
133
|
-
end
|
134
|
-
|
135
147
|
def obtain_current_time
|
136
148
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
137
149
|
end
|
@@ -139,6 +151,13 @@ class RedisClient
|
|
139
151
|
def calc_max_duration(timeout)
|
140
152
|
timeout.nil? || timeout < 0 ? 0 : timeout * 1_000_000
|
141
153
|
end
|
154
|
+
|
155
|
+
def start_over
|
156
|
+
@state_dict.each_value(&:close)
|
157
|
+
@state_dict.clear
|
158
|
+
@commands.each { |command| _call(command) }
|
159
|
+
nil
|
160
|
+
end
|
142
161
|
end
|
143
162
|
end
|
144
163
|
end
|
@@ -19,6 +19,8 @@ class RedisClient
|
|
19
19
|
ZERO_CURSOR_FOR_SCAN = '0'
|
20
20
|
TSF = ->(f, x) { f.nil? ? x : f.call(x) }.curry
|
21
21
|
|
22
|
+
private_constant :ZERO_CURSOR_FOR_SCAN, :TSF
|
23
|
+
|
22
24
|
def initialize(config, concurrent_worker, pool: nil, **kwargs)
|
23
25
|
@config = config.dup
|
24
26
|
@original_config = config.dup if config.connect_with_original_config
|
@@ -27,7 +29,7 @@ class RedisClient
|
|
27
29
|
@pool = pool
|
28
30
|
@client_kwargs = kwargs
|
29
31
|
@node = ::RedisClient::Cluster::Node.new(concurrent_worker, config: config, pool: pool, **kwargs)
|
30
|
-
|
32
|
+
renew_cluster_state
|
31
33
|
@command = ::RedisClient::Cluster::Command.load(@node.replica_clients.shuffle, slow_command_timeout: config.slow_command_timeout)
|
32
34
|
@command_builder = @config.command_builder
|
33
35
|
end
|
@@ -66,15 +68,21 @@ class RedisClient
|
|
66
68
|
rescue ::RedisClient::CircuitBreaker::OpenCircuitError
|
67
69
|
raise
|
68
70
|
rescue ::RedisClient::Cluster::Node::ReloadNeeded
|
69
|
-
|
71
|
+
renew_cluster_state
|
70
72
|
raise ::RedisClient::Cluster::NodeMightBeDown
|
73
|
+
rescue ::RedisClient::ConnectionError
|
74
|
+
renew_cluster_state
|
75
|
+
raise
|
76
|
+
rescue ::RedisClient::CommandError => e
|
77
|
+
renew_cluster_state if e.message.start_with?('CLUSTERDOWN Hash slot not served')
|
78
|
+
raise
|
71
79
|
rescue ::RedisClient::Cluster::ErrorCollection => e
|
72
80
|
raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
|
73
81
|
|
74
|
-
|
82
|
+
renew_cluster_state if e.errors.values.any? do |err|
|
75
83
|
next false if ::RedisClient::Cluster::ErrorIdentification.identifiable?(err) && @node.none? { |c| ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(err, c) }
|
76
84
|
|
77
|
-
err.message.start_with?('CLUSTERDOWN Hash slot not served')
|
85
|
+
err.message.start_with?('CLUSTERDOWN Hash slot not served') || err.is_a?(::RedisClient::ConnectionError)
|
78
86
|
end
|
79
87
|
|
80
88
|
raise
|
@@ -105,32 +113,29 @@ class RedisClient
|
|
105
113
|
rescue ::RedisClient::CommandError => e
|
106
114
|
raise unless ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(e, node)
|
107
115
|
|
116
|
+
retry_count -= 1
|
108
117
|
if e.message.start_with?('MOVED')
|
109
118
|
node = assign_redirection_node(e.message)
|
110
|
-
retry_count -= 1
|
111
119
|
retry if retry_count >= 0
|
112
120
|
elsif e.message.start_with?('ASK')
|
113
121
|
node = assign_asking_node(e.message)
|
114
|
-
retry_count -= 1
|
115
122
|
if retry_count >= 0
|
116
123
|
node.call('ASKING')
|
117
124
|
retry
|
118
125
|
end
|
119
126
|
elsif e.message.start_with?('CLUSTERDOWN Hash slot not served')
|
120
|
-
|
121
|
-
retry_count -= 1
|
127
|
+
renew_cluster_state
|
122
128
|
retry if retry_count >= 0
|
123
129
|
end
|
130
|
+
|
124
131
|
raise
|
125
132
|
rescue ::RedisClient::ConnectionError => e
|
126
133
|
raise unless ::RedisClient::Cluster::ErrorIdentification.client_owns_error?(e, node)
|
127
134
|
|
128
|
-
update_cluster_info!
|
129
|
-
|
130
|
-
raise if retry_count <= 0
|
131
|
-
|
132
135
|
retry_count -= 1
|
133
|
-
|
136
|
+
renew_cluster_state
|
137
|
+
retry if retry_count >= 0
|
138
|
+
raise
|
134
139
|
end
|
135
140
|
|
136
141
|
def scan(*command, seed: nil, **kwargs) # rubocop:disable Metrics/AbcSize
|
@@ -155,11 +160,16 @@ class RedisClient
|
|
155
160
|
client_index += 1 if result_cursor == 0
|
156
161
|
|
157
162
|
[((result_cursor << 8) + client_index).to_s, result_keys]
|
163
|
+
rescue ::RedisClient::ConnectionError
|
164
|
+
renew_cluster_state
|
165
|
+
raise
|
158
166
|
end
|
159
167
|
|
160
168
|
def assign_node(command)
|
161
|
-
|
162
|
-
|
169
|
+
handle_node_reload_error do
|
170
|
+
node_key = find_node_key(command)
|
171
|
+
@node.find_by(node_key)
|
172
|
+
end
|
163
173
|
end
|
164
174
|
|
165
175
|
def find_node_key_by_key(key, seed: nil, primary: false)
|
@@ -172,8 +182,10 @@ class RedisClient
|
|
172
182
|
end
|
173
183
|
|
174
184
|
def find_primary_node_by_slot(slot)
|
175
|
-
|
176
|
-
|
185
|
+
handle_node_reload_error do
|
186
|
+
node_key = @node.find_node_key_of_primary(slot)
|
187
|
+
@node.find_by(node_key)
|
188
|
+
end
|
177
189
|
end
|
178
190
|
|
179
191
|
def find_node_key(command, seed: nil)
|
@@ -198,14 +210,8 @@ class RedisClient
|
|
198
210
|
::RedisClient::Cluster::KeySlotConverter.convert(key)
|
199
211
|
end
|
200
212
|
|
201
|
-
def find_node(node_key
|
202
|
-
@node.find_by(node_key)
|
203
|
-
rescue ::RedisClient::Cluster::Node::ReloadNeeded
|
204
|
-
raise ::RedisClient::Cluster::NodeMightBeDown if retry_count <= 0
|
205
|
-
|
206
|
-
update_cluster_info!
|
207
|
-
retry_count -= 1
|
208
|
-
retry
|
213
|
+
def find_node(node_key)
|
214
|
+
handle_node_reload_error { @node.find_by(node_key) }
|
209
215
|
end
|
210
216
|
|
211
217
|
def command_exists?(name)
|
@@ -216,35 +222,37 @@ class RedisClient
|
|
216
222
|
_, slot, node_key = err_msg.split
|
217
223
|
slot = slot.to_i
|
218
224
|
@node.update_slot(slot, node_key)
|
219
|
-
|
225
|
+
handle_node_reload_error { @node.find_by(node_key) }
|
220
226
|
end
|
221
227
|
|
222
228
|
def assign_asking_node(err_msg)
|
223
229
|
_, _, node_key = err_msg.split
|
224
|
-
|
230
|
+
handle_node_reload_error { @node.find_by(node_key) }
|
225
231
|
end
|
226
232
|
|
227
233
|
def node_keys
|
228
234
|
@node.node_keys
|
229
235
|
end
|
230
236
|
|
237
|
+
def renew_cluster_state
|
238
|
+
@node.reload!
|
239
|
+
end
|
240
|
+
|
231
241
|
def close
|
232
242
|
@node.each(&:close)
|
233
243
|
end
|
234
244
|
|
235
245
|
private
|
236
246
|
|
237
|
-
def send_wait_command(method, command, args, retry_count:
|
247
|
+
def send_wait_command(method, command, args, retry_count: 1, &block) # rubocop:disable Metrics/AbcSize
|
238
248
|
@node.call_primaries(method, command, args).select { |r| r.is_a?(Integer) }.sum.then(&TSF.call(block))
|
239
249
|
rescue ::RedisClient::Cluster::ErrorCollection => e
|
240
250
|
raise if e.errors.any?(::RedisClient::CircuitBreaker::OpenCircuitError)
|
241
251
|
raise if retry_count <= 0
|
242
|
-
raise if e.errors.values.none?
|
243
|
-
err.message.include?('WAIT cannot be used with replica instances')
|
244
|
-
end
|
252
|
+
raise if e.errors.values.none? { |err| err.message.include?('WAIT cannot be used with replica instances') }
|
245
253
|
|
246
|
-
update_cluster_info!
|
247
254
|
retry_count -= 1
|
255
|
+
renew_cluster_state
|
248
256
|
retry
|
249
257
|
end
|
250
258
|
|
@@ -273,7 +281,7 @@ class RedisClient
|
|
273
281
|
end
|
274
282
|
end
|
275
283
|
|
276
|
-
def send_cluster_command(method, command, args, &block)
|
284
|
+
def send_cluster_command(method, command, args, &block) # rubocop:disable Metrics/AbcSize
|
277
285
|
case subcommand = ::RedisClient::Cluster::NormalizedCmdName.instance.get_by_subcommand(command)
|
278
286
|
when 'addslots', 'delslots', 'failover', 'forget', 'meet', 'replicate',
|
279
287
|
'reset', 'set-config-epoch', 'setslot'
|
@@ -282,7 +290,10 @@ class RedisClient
|
|
282
290
|
when 'getkeysinslot'
|
283
291
|
raise ArgumentError, command.join(' ') if command.size != 4
|
284
292
|
|
285
|
-
|
293
|
+
handle_node_reload_error do
|
294
|
+
node_key = @node.find_node_key_of_replica(command[2])
|
295
|
+
@node.find_by(node_key).public_send(method, *args, command, &block)
|
296
|
+
end
|
286
297
|
else assign_node(command).public_send(method, *args, command, &block)
|
287
298
|
end
|
288
299
|
end
|
@@ -335,6 +346,8 @@ class RedisClient
|
|
335
346
|
'del' => ['del', 1].freeze
|
336
347
|
}.freeze
|
337
348
|
|
349
|
+
private_constant :MULTIPLE_KEYS_COMMAND_TO_SINGLE
|
350
|
+
|
338
351
|
def send_multiple_keys_command(cmd, method, command, args, &block) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
339
352
|
# This implementation is prioritized performance rather than readability or so.
|
340
353
|
single_key_cmd, keys_step = MULTIPLE_KEYS_COMMAND_TO_SINGLE.fetch(cmd)
|
@@ -367,8 +380,14 @@ class RedisClient
|
|
367
380
|
block_given? ? yield(result) : result
|
368
381
|
end
|
369
382
|
|
370
|
-
def
|
371
|
-
|
383
|
+
def handle_node_reload_error(retry_count: 1)
|
384
|
+
yield
|
385
|
+
rescue ::RedisClient::Cluster::Node::ReloadNeeded
|
386
|
+
raise ::RedisClient::Cluster::NodeMightBeDown if retry_count <= 0
|
387
|
+
|
388
|
+
retry_count -= 1
|
389
|
+
renew_cluster_state
|
390
|
+
retry
|
372
391
|
end
|
373
392
|
end
|
374
393
|
end
|
@@ -7,9 +7,12 @@ class RedisClient
|
|
7
7
|
class Cluster
|
8
8
|
class Transaction
|
9
9
|
ConsistencyError = Class.new(::RedisClient::Error)
|
10
|
+
|
10
11
|
MAX_REDIRECTION = 2
|
11
12
|
EMPTY_ARRAY = [].freeze
|
12
13
|
|
14
|
+
private_constant :MAX_REDIRECTION, :EMPTY_ARRAY
|
15
|
+
|
13
16
|
def initialize(router, command_builder, node: nil, slot: nil, asking: false)
|
14
17
|
@router = router
|
15
18
|
@command_builder = command_builder
|
@@ -119,7 +122,7 @@ class RedisClient
|
|
119
122
|
end
|
120
123
|
end
|
121
124
|
|
122
|
-
def send_pipeline(client, redirect:)
|
125
|
+
def send_pipeline(client, redirect:) # rubocop:disable Metrics/AbcSize
|
123
126
|
replies = client.ensure_connected_cluster_scoped(retryable: @retryable) do |connection|
|
124
127
|
commands = @pipeline._commands
|
125
128
|
client.middlewares.call_pipelined(commands, client.config) do
|
@@ -135,6 +138,9 @@ class RedisClient
|
|
135
138
|
return if replies.last.nil?
|
136
139
|
|
137
140
|
coerce_results!(replies.last)
|
141
|
+
rescue ::RedisClient::ConnectionError
|
142
|
+
@router.renew_cluster_state if @watching_slot.nil?
|
143
|
+
raise
|
138
144
|
end
|
139
145
|
|
140
146
|
def coerce_results!(results, offset: 1)
|
@@ -164,6 +170,9 @@ class RedisClient
|
|
164
170
|
elsif err.message.start_with?('ASK')
|
165
171
|
node = @router.assign_asking_node(err.message)
|
166
172
|
try_asking(node) ? send_transaction(node, redirect: redirect - 1) : err
|
173
|
+
elsif err.message.start_with?('CLUSTERDOWN Hash slot not served')
|
174
|
+
@router.renew_cluster_state if @watching_slot.nil?
|
175
|
+
raise err
|
167
176
|
else
|
168
177
|
raise err
|
169
178
|
end
|
data/lib/redis_client/cluster.rb
CHANGED
@@ -20,13 +20,19 @@ class RedisClient
|
|
20
20
|
MAX_WORKERS = Integer(ENV.fetch('REDIS_CLIENT_MAX_THREADS', 5))
|
21
21
|
# It's used with slow queries of fetching meta data like CLUSTER NODES, COMMAND and so on.
|
22
22
|
SLOW_COMMAND_TIMEOUT = Float(ENV.fetch('REDIS_CLIENT_SLOW_COMMAND_TIMEOUT', -1))
|
23
|
+
# It affects to strike a balance between load and stability in initialization or changed states.
|
24
|
+
MAX_STARTUP_SAMPLE = Integer(ENV.fetch('REDIS_CLIENT_MAX_STARTUP_SAMPLE', 3))
|
25
|
+
|
26
|
+
private_constant :DEFAULT_HOST, :DEFAULT_PORT, :DEFAULT_SCHEME, :SECURE_SCHEME, :DEFAULT_NODES,
|
27
|
+
:VALID_SCHEMES, :VALID_NODES_KEYS, :MERGE_CONFIG_KEYS, :IGNORE_GENERIC_CONFIG_KEYS,
|
28
|
+
:MAX_WORKERS, :SLOW_COMMAND_TIMEOUT, :MAX_STARTUP_SAMPLE
|
23
29
|
|
24
30
|
InvalidClientConfigError = Class.new(::RedisClient::Error)
|
25
31
|
|
26
32
|
attr_reader :command_builder, :client_config, :replica_affinity, :slow_command_timeout,
|
27
|
-
:connect_with_original_config, :startup_nodes
|
33
|
+
:connect_with_original_config, :startup_nodes, :max_startup_sample
|
28
34
|
|
29
|
-
def initialize(
|
35
|
+
def initialize( # rubocop:disable Metrics/ParameterLists
|
30
36
|
nodes: DEFAULT_NODES,
|
31
37
|
replica: false,
|
32
38
|
replica_affinity: :random,
|
@@ -36,6 +42,7 @@ class RedisClient
|
|
36
42
|
client_implementation: ::RedisClient::Cluster, # for redis gem
|
37
43
|
slow_command_timeout: SLOW_COMMAND_TIMEOUT,
|
38
44
|
command_builder: ::RedisClient::CommandBuilder,
|
45
|
+
max_startup_sample: MAX_STARTUP_SAMPLE,
|
39
46
|
**client_config
|
40
47
|
)
|
41
48
|
|
@@ -51,6 +58,7 @@ class RedisClient
|
|
51
58
|
@connect_with_original_config = connect_with_original_config
|
52
59
|
@client_implementation = client_implementation
|
53
60
|
@slow_command_timeout = slow_command_timeout
|
61
|
+
@max_startup_sample = max_startup_sample
|
54
62
|
end
|
55
63
|
|
56
64
|
def inspect
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: redis-cluster-client
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.11.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Taishi Kasuga
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2024-
|
11
|
+
date: 2024-09-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: redis-client
|
@@ -77,7 +77,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
77
77
|
- !ruby/object:Gem::Version
|
78
78
|
version: '0'
|
79
79
|
requirements: []
|
80
|
-
rubygems_version: 3.5.
|
80
|
+
rubygems_version: 3.5.16
|
81
81
|
signing_key:
|
82
82
|
specification_version: 4
|
83
83
|
summary: A Redis cluster client for Ruby
|