2
2
3
3
require 'redis_client'
4
4
require 'redis_client/cluster/errors'
5
+ require 'redis_client/connection_mixin'
5
6
require 'redis_client/middlewares'
6
7
require 'redis_client/pooled'
7
8
8
9
class RedisClient
9
10
class Cluster
10
11
class Pipeline
12
+ class Extended < ::RedisClient ::Pipeline
13
+ attr_reader :outer_indices
14
+
15
+ def initialize ( command_builder )
16
+ super
17
+ @outer_indices = nil
18
+ end
19
+
20
+ def add_outer_index ( index )
21
+ @outer_indices ||= [ ]
22
+ @outer_indices << index
23
+ end
24
+
25
+ def get_inner_index ( outer_index )
26
+ @outer_indices &.find_index ( outer_index )
27
+ end
28
+
29
+ def get_callee_method ( inner_index )
30
+ if @timeouts . is_a? ( Array ) && !@timeouts [ inner_index ] . nil?
31
+ :blocking_call_v
32
+ elsif _retryable?
33
+ :call_once_v
34
+ else
35
+ :call_v
36
+ end
37
+ end
38
+
39
+ def get_command ( inner_index )
40
+ @commands . is_a? ( Array ) ? @commands [ inner_index ] : nil
41
+ end
42
+
43
+ def get_timeout ( inner_index )
44
+ @timeouts . is_a? ( Array ) ? @timeouts [ inner_index ] : nil
45
+ end
46
+
47
+ def get_block ( inner_index )
48
+ @blocks . is_a? ( Array ) ? @blocks [ inner_index ] : nil
49
+ end
50
+ end
51
+
52
+ ::RedisClient ::ConnectionMixin . module_eval do
53
+ def call_pipelined_aware_of_redirection ( commands , timeouts ) # rubocop:disable Metrics/AbcSize
54
+ size = commands . size
55
+ results = Array . new ( commands . size )
56
+ @pending_reads += size
57
+ write_multi ( commands )
58
+
59
+ redirection_indices = nil
60
+ size . times do |index |
61
+ timeout = timeouts && timeouts [ index ]
62
+ result = read ( timeout )
63
+ @pending_reads -= 1
64
+ if result . is_a? ( CommandError )
65
+ result . _set_command ( commands [ index ] )
66
+ if result . message . start_with? ( 'MOVED' , 'ASK' )
67
+ redirection_indices ||= [ ]
68
+ redirection_indices << index
69
+ end
70
+ end
71
+
72
+ results [ index ] = result
73
+ end
74
+
75
+ return results if redirection_indices . nil?
76
+
77
+ err = ::RedisClient ::Cluster ::Pipeline ::RedirectionNeeded . new
78
+ err . replies = results
79
+ err . indices = redirection_indices
80
+ raise err
81
+ end
82
+ end
83
+
11
84
ReplySizeError = Class . new ( ::RedisClient ::Error )
85
+
86
+ class RedirectionNeeded < ::RedisClient ::Error
87
+ attr_accessor :replies , :indices
88
+ end
89
+
12
90
MAX_THREADS = Integer ( ENV . fetch ( 'REDIS_CLIENT_MAX_THREADS' , 5 ) )
13
91
14
92
def initialize ( router , command_builder , seed : Random . new_seed )
15
93
@router = router
16
94
@command_builder = command_builder
17
95
@seed = seed
18
- @pipelines = @indices = nil
96
+ @pipelines = nil
19
97
@size = 0
20
98
end
21
99
22
100
def call ( *args , **kwargs , &block )
23
101
command = @command_builder . generate ( args , kwargs )
24
102
node_key = @router . find_node_key ( command , seed : @seed )
25
- get_pipeline ( node_key ) . call_v ( command , &block )
26
- index_pipeline ( node_key )
103
+ append_pipeline ( node_key ) . call_v ( command , &block )
27
104
end
28
105
29
106
def call_v ( args , &block )
30
107
command = @command_builder . generate ( args )
31
108
node_key = @router . find_node_key ( command , seed : @seed )
32
- get_pipeline ( node_key ) . call_v ( command , &block )
33
- index_pipeline ( node_key )
109
+ append_pipeline ( node_key ) . call_v ( command , &block )
34
110
end
35
111
36
112
def call_once ( *args , **kwargs , &block )
37
113
command = @command_builder . generate ( args , kwargs )
38
114
node_key = @router . find_node_key ( command , seed : @seed )
39
- get_pipeline ( node_key ) . call_once_v ( command , &block )
40
- index_pipeline ( node_key )
115
+ append_pipeline ( node_key ) . call_once_v ( command , &block )
41
116
end
42
117
43
118
def call_once_v ( args , &block )
44
119
command = @command_builder . generate ( args )
45
120
node_key = @router . find_node_key ( command , seed : @seed )
46
- get_pipeline ( node_key ) . call_once_v ( command , &block )
47
- index_pipeline ( node_key )
121
+ append_pipeline ( node_key ) . call_once_v ( command , &block )
48
122
end
49
123
50
124
def blocking_call ( timeout , *args , **kwargs , &block )
51
125
command = @command_builder . generate ( args , kwargs )
52
126
node_key = @router . find_node_key ( command , seed : @seed )
53
- get_pipeline ( node_key ) . blocking_call_v ( timeout , command , &block )
54
- index_pipeline ( node_key )
127
+ append_pipeline ( node_key ) . blocking_call_v ( timeout , command , &block )
55
128
end
56
129
57
130
def blocking_call_v ( timeout , args , &block )
58
131
command = @command_builder . generate ( args )
59
132
node_key = @router . find_node_key ( command , seed : @seed )
60
- get_pipeline ( node_key ) . blocking_call_v ( timeout , command , &block )
61
- index_pipeline ( node_key )
133
+ append_pipeline ( node_key ) . blocking_call_v ( timeout , command , &block )
62
134
end
63
135
64
136
def empty?
65
137
@size . zero?
66
138
end
67
139
68
- # TODO: https://github.com/redis-rb/redis-cluster-client/issues/37 handle redirections
69
140
def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
70
141
all_replies = errors = nil
71
142
@pipelines &.each_slice ( MAX_THREADS ) do |chuncked_pipelines |
@@ -77,40 +148,47 @@ def execute # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Met
77
148
raise ReplySizeError , "commands: #{ pl . _size } , replies: #{ replies . size } " if pl . _size != replies . size
78
149
79
150
Thread . current . thread_variable_set ( :replies , replies )
151
+ rescue ::RedisClient ::Cluster ::Pipeline ::RedirectionNeeded => e
152
+ Thread . current . thread_variable_set ( :redirection_needed , e )
80
153
rescue StandardError => e
81
154
Thread . current . thread_variable_set ( :error , e )
82
155
end
83
156
end
84
157
85
158
threads . each do |t |
86
159
t . join
160
+
87
161
if t . thread_variable? ( :replies )
88
162
all_replies ||= Array . new ( @size )
89
- @indices [ t . thread_variable_get ( :node_key ) ] . each_with_index { |gi , i | all_replies [ gi ] = t . thread_variable_get ( :replies ) [ i ] }
163
+ @pipelines [ t . thread_variable_get ( :node_key ) ]
164
+ . outer_indices
165
+ . each_with_index { |outer , inner | all_replies [ outer ] = t . thread_variable_get ( :replies ) [ inner ] }
166
+ elsif t . thread_variable? ( :redirection_needed )
167
+ all_replies ||= Array . new ( @size )
168
+ pipeline = @pipelines [ t . thread_variable_get ( :node_key ) ]
169
+ err = t . thread_variable_get ( :redirection_needed )
170
+ err . indices . each { |i | err . replies [ i ] = handle_redirection ( err . replies [ i ] , pipeline , i ) }
171
+ pipeline . outer_indices . each_with_index { |outer , inner | all_replies [ outer ] = err . replies [ inner ] }
90
172
elsif t . thread_variable? ( :error )
91
173
errors ||= { }
92
174
errors [ t . thread_variable_get ( :node_key ) ] = t . thread_variable_get ( :error )
93
175
end
94
176
end
95
177
end
96
178
97
- return all_replies if errors . nil?
179
+ raise :: RedisClient :: Cluster :: ErrorCollection , errors unless errors . nil?
98
180
99
- raise :: RedisClient :: Cluster :: ErrorCollection , errors
181
+ all_replies
100
182
end
101
183
102
184
private
103
185
104
- def get_pipeline ( node_key )
186
+ def append_pipeline ( node_key )
105
187
@pipelines ||= { }
106
- @pipelines [ node_key ] ||= ::RedisClient ::Pipeline . new ( @command_builder )
107
- end
108
-
109
- def index_pipeline ( node_key )
110
- @indices ||= { }
111
- @indices [ node_key ] ||= [ ]
112
- @indices [ node_key ] << @size
188
+ @pipelines [ node_key ] ||= ::RedisClient ::Cluster ::Pipeline ::Extended . new ( @command_builder )
189
+ @pipelines [ node_key ] . add_outer_index ( @size )
113
190
@size += 1
191
+ @pipelines [ node_key ]
114
192
end
115
193
116
194
def do_pipelining ( client , pipeline )
@@ -125,12 +203,52 @@ def send_pipeline(client, pipeline)
125
203
results = client . send ( :ensure_connected , retryable : pipeline . _retryable? ) do |connection |
126
204
commands = pipeline . _commands
127
205
::RedisClient ::Middlewares . call_pipelined ( commands , client . config ) do
128
- connection . call_pipelined ( commands , pipeline . _timeouts )
206
+ connection . call_pipelined_aware_of_redirection ( commands , pipeline . _timeouts )
129
207
end
130
208
end
131
209
132
210
pipeline . _coerce! ( results )
133
211
end
212
+
213
+ def handle_redirection ( err , pipeline , inner_index )
214
+ return err unless err . is_a? ( ::RedisClient ::CommandError )
215
+
216
+ if err . message . start_with? ( 'MOVED' )
217
+ node = @router . assign_redirection_node ( err . message )
218
+ try_redirection ( node , pipeline , inner_index )
219
+ elsif err . message . start_with? ( 'ASK' )
220
+ node = @router . assign_asking_node ( err . message )
221
+ try_asking ( node ) ? try_redirection ( node , pipeline , inner_index ) : err
222
+ else
223
+ err
224
+ end
225
+ end
226
+
227
+ def try_redirection ( node , pipeline , inner_index )
228
+ redirect_command ( node , pipeline , inner_index )
229
+ rescue StandardError => e
230
+ e
231
+ end
232
+
233
+ def redirect_command ( node , pipeline , inner_index )
234
+ method = pipeline . get_callee_method ( inner_index )
235
+ command = pipeline . get_command ( inner_index )
236
+ timeout = pipeline . get_timeout ( inner_index )
237
+ block = pipeline . get_block ( inner_index )
238
+ args = timeout . nil? ? [ ] : [ timeout ]
239
+
240
+ if block . nil?
241
+ @router . try_send ( node , method , command , args )
242
+ else
243
+ @router . try_send ( node , method , command , args , &block )
244
+ end
245
+ end
246
+
247
+ def try_asking ( node )
248
+ node . call ( 'ASKING' ) == 'OK'
249
+ rescue StandardError
250
+ false
251
+ end
134
252
end
135
253
end
136
254
end
0 commit comments