redis-cluster-client 0.11.0 → 0.13.5
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 +47 -75
- data/lib/redis_client/cluster/concurrent_worker/on_demand.rb +2 -0
- data/lib/redis_client/cluster/concurrent_worker/pooled.rb +2 -0
- data/lib/redis_client/cluster/concurrent_worker.rb +4 -6
- data/lib/redis_client/cluster/errors.rb +39 -19
- data/lib/redis_client/cluster/key_slot_converter.rb +3 -1
- data/lib/redis_client/cluster/node/base_topology.rb +3 -1
- data/lib/redis_client/cluster/node/latency_replica.rb +3 -1
- data/lib/redis_client/cluster/node.rb +77 -14
- data/lib/redis_client/cluster/node_key.rb +2 -0
- data/lib/redis_client/cluster/noop_command_builder.rb +13 -0
- data/lib/redis_client/cluster/optimistic_locking.rb +25 -10
- data/lib/redis_client/cluster/pipeline.rb +53 -18
- data/lib/redis_client/cluster/pub_sub.rb +70 -31
- data/lib/redis_client/cluster/router.rb +282 -134
- data/lib/redis_client/cluster/transaction.rb +22 -11
- data/lib/redis_client/cluster.rb +24 -15
- data/lib/redis_client/cluster_config.rb +44 -11
- metadata +7 -11
- data/lib/redis_client/cluster/normalized_cmd_name.rb +0 -69
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'redis_client'
|
4
4
|
require 'redis_client/cluster/errors'
|
5
|
+
require 'redis_client/cluster/noop_command_builder'
|
5
6
|
require 'redis_client/connection_mixin'
|
6
7
|
require 'redis_client/middlewares'
|
7
8
|
require 'redis_client/pooled'
|
@@ -55,32 +56,48 @@ class RedisClient
|
|
55
56
|
results = Array.new(commands.size)
|
56
57
|
@pending_reads += size
|
57
58
|
write_multi(commands)
|
59
|
+
redirection_indices = stale_cluster_state = first_exception = nil
|
58
60
|
|
59
|
-
redirection_indices = nil
|
60
|
-
first_exception = nil
|
61
61
|
size.times do |index|
|
62
62
|
timeout = timeouts && timeouts[index]
|
63
|
-
result = read(timeout)
|
63
|
+
result = read(connection_timeout(timeout))
|
64
64
|
@pending_reads -= 1
|
65
|
+
|
65
66
|
if result.is_a?(::RedisClient::Error)
|
66
67
|
result._set_command(commands[index])
|
68
|
+
result._set_config(config)
|
69
|
+
|
67
70
|
if result.is_a?(::RedisClient::CommandError) && result.message.start_with?('MOVED', 'ASK')
|
68
71
|
redirection_indices ||= []
|
69
72
|
redirection_indices << index
|
70
73
|
elsif exception
|
71
74
|
first_exception ||= result
|
72
75
|
end
|
76
|
+
|
77
|
+
stale_cluster_state = true if result.message.start_with?('CLUSTERDOWN')
|
73
78
|
end
|
79
|
+
|
74
80
|
results[index] = result
|
75
81
|
end
|
76
82
|
|
77
|
-
|
78
|
-
|
83
|
+
if redirection_indices
|
84
|
+
err = ::RedisClient::Cluster::Pipeline::RedirectionNeeded.new
|
85
|
+
err.replies = results
|
86
|
+
err.indices = redirection_indices
|
87
|
+
err.first_exception = first_exception
|
88
|
+
raise err
|
89
|
+
end
|
90
|
+
|
91
|
+
if stale_cluster_state
|
92
|
+
err = ::RedisClient::Cluster::Pipeline::StaleClusterState.new
|
93
|
+
err.replies = results
|
94
|
+
err.first_exception = first_exception
|
95
|
+
raise err
|
96
|
+
end
|
97
|
+
|
98
|
+
raise first_exception if first_exception
|
79
99
|
|
80
|
-
|
81
|
-
err.replies = results
|
82
|
-
err.indices = redirection_indices
|
83
|
-
raise err
|
100
|
+
results
|
84
101
|
end
|
85
102
|
end
|
86
103
|
|
@@ -92,10 +109,14 @@ class RedisClient
|
|
92
109
|
end
|
93
110
|
end
|
94
111
|
|
95
|
-
ReplySizeError = Class.new(::RedisClient::Error)
|
112
|
+
ReplySizeError = Class.new(::RedisClient::Cluster::Error)
|
96
113
|
|
97
|
-
class
|
98
|
-
attr_accessor :replies, :
|
114
|
+
class StaleClusterState < ::RedisClient::Cluster::Error
|
115
|
+
attr_accessor :replies, :first_exception
|
116
|
+
end
|
117
|
+
|
118
|
+
class RedirectionNeeded < ::RedisClient::Cluster::Error
|
119
|
+
attr_accessor :replies, :indices, :first_exception
|
99
120
|
end
|
100
121
|
|
101
122
|
def initialize(router, command_builder, concurrent_worker, exception:, seed: Random.new_seed)
|
@@ -162,14 +183,18 @@ class RedisClient
|
|
162
183
|
end
|
163
184
|
end
|
164
185
|
|
165
|
-
all_replies = errors = required_redirections = nil
|
186
|
+
all_replies = errors = required_redirections = cluster_state_errors = nil
|
166
187
|
|
167
188
|
work_group.each do |node_key, v|
|
168
189
|
case v
|
169
190
|
when ::RedisClient::Cluster::Pipeline::RedirectionNeeded
|
170
191
|
required_redirections ||= {}
|
171
192
|
required_redirections[node_key] = v
|
193
|
+
when ::RedisClient::Cluster::Pipeline::StaleClusterState
|
194
|
+
cluster_state_errors ||= {}
|
195
|
+
cluster_state_errors[node_key] = v
|
172
196
|
when StandardError
|
197
|
+
cluster_state_errors ||= {} if v.is_a?(::RedisClient::ConnectionError)
|
173
198
|
errors ||= {}
|
174
199
|
errors[node_key] = v
|
175
200
|
else
|
@@ -179,15 +204,25 @@ class RedisClient
|
|
179
204
|
end
|
180
205
|
|
181
206
|
work_group.close
|
182
|
-
|
207
|
+
@router.renew_cluster_state if cluster_state_errors
|
208
|
+
raise ::RedisClient::Cluster::ErrorCollection.with_errors(errors).with_config(@router.config) unless errors.nil?
|
183
209
|
|
184
210
|
required_redirections&.each do |node_key, v|
|
211
|
+
raise v.first_exception if v.first_exception
|
212
|
+
|
185
213
|
all_replies ||= Array.new(@size)
|
186
214
|
pipeline = @pipelines[node_key]
|
187
215
|
v.indices.each { |i| v.replies[i] = handle_redirection(v.replies[i], pipeline, i) }
|
188
216
|
pipeline.outer_indices.each_with_index { |outer, inner| all_replies[outer] = v.replies[inner] }
|
189
217
|
end
|
190
218
|
|
219
|
+
cluster_state_errors&.each do |node_key, v|
|
220
|
+
raise v.first_exception if v.first_exception
|
221
|
+
|
222
|
+
all_replies ||= Array.new(@size)
|
223
|
+
@pipelines[node_key].outer_indices.each_with_index { |outer, inner| all_replies[outer] = v.replies[inner] }
|
224
|
+
end
|
225
|
+
|
191
226
|
all_replies
|
192
227
|
end
|
193
228
|
|
@@ -195,7 +230,7 @@ class RedisClient
|
|
195
230
|
|
196
231
|
def append_pipeline(node_key)
|
197
232
|
@pipelines ||= {}
|
198
|
-
@pipelines[node_key] ||= ::RedisClient::Cluster::Pipeline::Extended.new(
|
233
|
+
@pipelines[node_key] ||= ::RedisClient::Cluster::Pipeline::Extended.new(::RedisClient::Cluster::NoopCommandBuilder)
|
199
234
|
@pipelines[node_key].add_outer_index(@size)
|
200
235
|
@size += 1
|
201
236
|
@pipelines[node_key]
|
@@ -248,14 +283,14 @@ class RedisClient
|
|
248
283
|
args = timeout.nil? ? [] : [timeout]
|
249
284
|
|
250
285
|
if block.nil?
|
251
|
-
@router.
|
286
|
+
@router.send_command_to_node(node, method, command, args)
|
252
287
|
else
|
253
|
-
@router.
|
288
|
+
@router.send_command_to_node(node, method, command, args, &block)
|
254
289
|
end
|
255
290
|
end
|
256
291
|
|
257
292
|
def try_asking(node)
|
258
|
-
node.call('
|
293
|
+
node.call('asking') == 'OK'
|
259
294
|
rescue StandardError
|
260
295
|
false
|
261
296
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'redis_client'
|
4
|
-
require 'redis_client/cluster/
|
4
|
+
require 'redis_client/cluster/errors'
|
5
5
|
|
6
6
|
class RedisClient
|
7
7
|
class Cluster
|
@@ -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
|
@@ -32,11 +34,15 @@ class RedisClient
|
|
32
34
|
# Ruby VM allocates 1 MB memory as a stack for a thread.
|
33
35
|
# It is a fixed size but we can modify the size with some environment variables.
|
34
36
|
# So it consumes memory 1 MB multiplied a number of workers.
|
35
|
-
Thread.new(client, queue) do |pubsub, q|
|
37
|
+
Thread.new(client, queue, nil) do |pubsub, q, prev_err|
|
36
38
|
loop do
|
37
39
|
q << pubsub.next_event
|
40
|
+
prev_err = nil
|
38
41
|
rescue StandardError => e
|
42
|
+
next sleep 0.005 if e.instance_of?(prev_err.class) && e.message == prev_err&.message
|
43
|
+
|
39
44
|
q << e
|
45
|
+
prev_err = e
|
40
46
|
end
|
41
47
|
end
|
42
48
|
end
|
@@ -44,32 +50,40 @@ class RedisClient
|
|
44
50
|
|
45
51
|
BUF_SIZE = Integer(ENV.fetch('REDIS_CLIENT_PUBSUB_BUF_SIZE', 1024))
|
46
52
|
|
53
|
+
private_constant :BUF_SIZE
|
54
|
+
|
47
55
|
def initialize(router, command_builder)
|
48
56
|
@router = router
|
49
57
|
@command_builder = command_builder
|
50
58
|
@queue = SizedQueue.new(BUF_SIZE)
|
51
59
|
@state_dict = {}
|
60
|
+
@commands = []
|
52
61
|
end
|
53
62
|
|
54
63
|
def call(*args, **kwargs)
|
55
|
-
|
64
|
+
command = @command_builder.generate(args, kwargs)
|
65
|
+
_call(command)
|
66
|
+
@commands << command
|
56
67
|
nil
|
57
68
|
end
|
58
69
|
|
59
70
|
def call_v(command)
|
60
|
-
|
71
|
+
command = @command_builder.generate(command)
|
72
|
+
_call(command)
|
73
|
+
@commands << command
|
61
74
|
nil
|
62
75
|
end
|
63
76
|
|
64
77
|
def close
|
65
78
|
@state_dict.each_value(&:close)
|
66
79
|
@state_dict.clear
|
80
|
+
@commands.clear
|
67
81
|
@queue.clear
|
68
82
|
@queue.close
|
69
83
|
nil
|
70
84
|
end
|
71
85
|
|
72
|
-
def next_event(timeout = nil)
|
86
|
+
def next_event(timeout = nil) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
|
73
87
|
@state_dict.each_value(&:ensure_worker)
|
74
88
|
max_duration = calc_max_duration(timeout)
|
75
89
|
starting = obtain_current_time
|
@@ -78,6 +92,11 @@ class RedisClient
|
|
78
92
|
break if max_duration > 0 && obtain_current_time - starting > max_duration
|
79
93
|
|
80
94
|
case event = @queue.pop(true)
|
95
|
+
when ::RedisClient::CommandError
|
96
|
+
raise event unless event.message.start_with?('MOVED', 'CLUSTERDOWN')
|
97
|
+
|
98
|
+
break start_over
|
99
|
+
when ::RedisClient::ConnectionError then break start_over
|
81
100
|
when StandardError then raise event
|
82
101
|
when Array then break event
|
83
102
|
end
|
@@ -88,22 +107,39 @@ class RedisClient
|
|
88
107
|
|
89
108
|
private
|
90
109
|
|
91
|
-
def _call(command)
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
110
|
+
def _call(command) # rubocop:disable Metrics/AbcSize
|
111
|
+
if command.first.casecmp('subscribe').zero?
|
112
|
+
call_to_single_state(command)
|
113
|
+
elsif command.first.casecmp('psubscribe').zero?
|
114
|
+
call_to_single_state(command)
|
115
|
+
elsif command.first.casecmp('ssubscribe').zero?
|
116
|
+
call_to_single_state(command)
|
117
|
+
elsif command.first.casecmp('unsubscribe').zero?
|
118
|
+
call_to_all_states(command)
|
119
|
+
elsif command.first.casecmp('punsubscribe').zero?
|
120
|
+
call_to_all_states(command)
|
121
|
+
elsif command.first.casecmp('sunsubscribe').zero?
|
122
|
+
call_for_sharded_states(command)
|
123
|
+
else
|
124
|
+
call_to_single_state(command)
|
97
125
|
end
|
98
126
|
end
|
99
127
|
|
100
128
|
def call_to_single_state(command)
|
101
129
|
node_key = @router.find_node_key(command)
|
102
|
-
|
130
|
+
|
131
|
+
handle_connection_error(node_key) do
|
132
|
+
@state_dict[node_key] ||= State.new(@router.find_node(node_key).pubsub, @queue)
|
133
|
+
@state_dict[node_key].call(command)
|
134
|
+
end
|
103
135
|
end
|
104
136
|
|
105
137
|
def call_to_all_states(command)
|
106
|
-
@state_dict.
|
138
|
+
@state_dict.each do |node_key, state|
|
139
|
+
handle_connection_error(node_key, ignore: true) do
|
140
|
+
state.call(command)
|
141
|
+
end
|
142
|
+
end
|
107
143
|
end
|
108
144
|
|
109
145
|
def call_for_sharded_states(command)
|
@@ -114,24 +150,6 @@ class RedisClient
|
|
114
150
|
end
|
115
151
|
end
|
116
152
|
|
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
153
|
def obtain_current_time
|
136
154
|
Process.clock_gettime(Process::CLOCK_MONOTONIC, :microsecond)
|
137
155
|
end
|
@@ -139,6 +157,27 @@ class RedisClient
|
|
139
157
|
def calc_max_duration(timeout)
|
140
158
|
timeout.nil? || timeout < 0 ? 0 : timeout * 1_000_000
|
141
159
|
end
|
160
|
+
|
161
|
+
def handle_connection_error(node_key, ignore: false)
|
162
|
+
yield
|
163
|
+
rescue ::RedisClient::ConnectionError
|
164
|
+
@state_dict[node_key]&.close
|
165
|
+
@state_dict.delete(node_key)
|
166
|
+
@router.renew_cluster_state
|
167
|
+
raise unless ignore
|
168
|
+
end
|
169
|
+
|
170
|
+
def start_over
|
171
|
+
loop do
|
172
|
+
@router.renew_cluster_state
|
173
|
+
@state_dict.each_value(&:close)
|
174
|
+
@state_dict.clear
|
175
|
+
@commands.each { |command| _call(command) }
|
176
|
+
break
|
177
|
+
rescue ::RedisClient::ConnectionError, ::RedisClient::Cluster::NodeMightBeDown
|
178
|
+
sleep 1.0
|
179
|
+
end
|
180
|
+
end
|
142
181
|
end
|
143
182
|
end
|
144
183
|
end
|