@@ -22,10 +22,10 @@ class Node
22
22
SLOT_SIZE = 16_384
23
23
MIN_SLOT = 0
24
24
MAX_SLOT = SLOT_SIZE - 1
25
- IGNORE_GENERIC_CONFIG_KEYS = %i[ url host port path ] . freeze
26
25
DEAD_FLAGS = %w[ fail? fail handshake noaddr noflags ] . freeze
27
26
ROLE_FLAGS = %w[ master slave ] . freeze
28
27
EMPTY_ARRAY = [ ] . freeze
28
+ EMPTY_HASH = { } . freeze
29
29
30
30
ReloadNeeded = Class . new ( ::RedisClient ::Error )
31
31
@@ -92,119 +92,19 @@ def build_connection_prelude
92
92
end
93
93
end
94
94
95
- class << self
96
- def load_info ( options , concurrent_worker , slow_command_timeout : -1 , **kwargs ) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
97
- raise ::RedisClient ::Cluster ::InitialSetupError , [ ] if options . nil? || options . empty?
98
-
99
- startup_size = options . size > MAX_STARTUP_SAMPLE ? MAX_STARTUP_SAMPLE : options . size
100
- startup_options = options . to_a . sample ( startup_size ) . to_h
101
- startup_nodes = ::RedisClient ::Cluster ::Node . new ( startup_options , concurrent_worker , **kwargs )
102
- work_group = concurrent_worker . new_group ( size : startup_size )
103
-
104
- startup_nodes . each_with_index do |raw_client , i |
105
- work_group . push ( i , raw_client ) do |client |
106
- regular_timeout = client . read_timeout
107
- client . read_timeout = slow_command_timeout > 0.0 ? slow_command_timeout : regular_timeout
108
- reply = client . call ( 'CLUSTER' , 'NODES' )
109
- client . read_timeout = regular_timeout
110
- parse_cluster_node_reply ( reply )
111
- rescue StandardError => e
112
- e
113
- ensure
114
- client &.close
115
- end
116
- end
117
-
118
- node_info_list = errors = nil
119
-
120
- work_group . each do |i , v |
121
- case v
122
- when StandardError
123
- errors ||= Array . new ( startup_size )
124
- errors [ i ] = v
125
- else
126
- node_info_list ||= Array . new ( startup_size )
127
- node_info_list [ i ] = v
128
- end
129
- end
130
-
131
- work_group . close
132
-
133
- raise ::RedisClient ::Cluster ::InitialSetupError , errors if node_info_list . nil?
134
-
135
- grouped = node_info_list . compact . group_by do |info_list |
136
- info_list . sort_by! ( &:id )
137
- info_list . each_with_object ( String . new ( capacity : 128 * info_list . size ) ) do |e , a |
138
- a << e . id << e . node_key << e . role << e . primary_id << e . config_epoch
139
- end
140
- end
141
-
142
- grouped . max_by { |_ , v | v . size } [ 1 ] . first . freeze
143
- end
144
-
145
- private
146
-
147
- # @see https://redis.io/commands/cluster-nodes/
148
- # @see https://github.com/redis/redis/blob/78960ad57b8a5e6af743d789ed8fd767e37d42b8/src/cluster.c#L4660-L4683
149
- def parse_cluster_node_reply ( reply ) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
150
- reply . each_line ( "\n " , chomp : true ) . filter_map do |line |
151
- fields = line . split
152
- flags = fields [ 2 ] . split ( ',' )
153
- next unless fields [ 7 ] == 'connected' && ( flags & DEAD_FLAGS ) . empty?
154
-
155
- slots = if fields [ 8 ] . nil?
156
- EMPTY_ARRAY
157
- else
158
- fields [ 8 ..] . reject { |str | str . start_with? ( '[' ) }
159
- . map { |str | str . split ( '-' ) . map { |s | Integer ( s ) } }
160
- . map { |a | a . size == 1 ? a << a . first : a }
161
- . map ( &:sort )
162
- end
163
-
164
- ::RedisClient ::Cluster ::Node ::Info . new (
165
- id : fields [ 0 ] ,
166
- node_key : parse_node_key ( fields [ 1 ] ) ,
167
- role : ( flags & ROLE_FLAGS ) . first ,
168
- primary_id : fields [ 3 ] ,
169
- ping_sent : fields [ 4 ] ,
170
- pong_recv : fields [ 5 ] ,
171
- config_epoch : fields [ 6 ] ,
172
- link_state : fields [ 7 ] ,
173
- slots : slots
174
- )
175
- end
176
- end
177
-
178
- # As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
179
- # node_key should use hostname if present in CLUSTER NODES output.
180
- #
181
- # See https://redis.io/commands/cluster-nodes/ for details on the output format.
182
- # node_address matches fhe format: <ip:port@cport[,hostname[,auxiliary_field=value]*]>
183
- def parse_node_key ( node_address )
184
- ip_chunk , hostname , _auxiliaries = node_address . split ( ',' )
185
- ip_port_string = ip_chunk . split ( '@' ) . first
186
- return ip_port_string if hostname . nil? || hostname . empty?
187
-
188
- port = ip_port_string . split ( ':' ) [ 1 ]
189
- "#{ hostname } :#{ port } "
190
- end
191
- end
192
-
193
95
def initialize (
194
- options ,
195
96
concurrent_worker ,
196
- node_info_list : [ ] ,
197
- with_replica : false ,
198
- replica_affinity : :random ,
97
+ config :,
199
98
pool : nil ,
200
99
**kwargs
201
100
)
202
101
203
102
@concurrent_worker = concurrent_worker
204
- @slots = build_slot_node_mappings ( node_info_list )
205
- @replications = build_replication_mappings ( node_info_list )
206
- klass = make_topology_class ( with_replica , replica_affinity )
207
- @topology = klass . new ( @replications , options , pool , @concurrent_worker , **kwargs )
103
+ @slots = build_slot_node_mappings ( EMPTY_ARRAY )
104
+ @replications = build_replication_mappings ( EMPTY_ARRAY )
105
+ klass = make_topology_class ( config . use_replica? , config . replica_affinity )
106
+ @topology = klass . new ( pool , @concurrent_worker , **kwargs )
107
+ @config = config
208
108
@mutex = Mutex . new
209
109
end
210
110
@@ -255,6 +155,14 @@ def clients_for_scanning(seed: nil)
255
155
@topology . clients_for_scanning ( seed : seed ) . values . sort_by { |c | "#{ c . config . host } -#{ c . config . port } " }
256
156
end
257
157
158
+ def clients
159
+ @topology . clients . values
160
+ end
161
+
162
+ def primary_clients
163
+ @topology . primary_clients . values
164
+ end
165
+
258
166
def replica_clients
259
167
@topology . replica_clients . values
260
168
end
@@ -292,6 +200,20 @@ def update_slot(slot, node_key)
292
200
end
293
201
end
294
202
203
+ def reload!
204
+ with_reload_lock do
205
+ with_startup_clients ( MAX_STARTUP_SAMPLE ) do |startup_clients |
206
+ @node_info = refetch_node_info_list ( startup_clients )
207
+ @node_configs = @node_info . to_h do |node_info |
208
+ [ node_info . node_key , @config . client_config_for_node ( node_info . node_key ) ]
209
+ end
210
+ @slots = build_slot_node_mappings ( @node_info )
211
+ @replications = build_replication_mappings ( @node_info )
212
+ @topology . process_topology_update! ( @replications , @node_configs )
213
+ end
214
+ end
215
+ end
216
+
295
217
private
296
218
297
219
def make_topology_class ( with_replica , replica_affinity )
@@ -378,6 +300,137 @@ def try_map(clients, &block) # rubocop:disable Metrics/AbcSize, Metrics/Cyclomat
378
300
379
301
[ results , errors ]
380
302
end
303
+
304
+ def refetch_node_info_list ( startup_clients ) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
305
+ startup_size = startup_clients . size
306
+ work_group = @concurrent_worker . new_group ( size : startup_size )
307
+
308
+ startup_clients . each_with_index do |raw_client , i |
309
+ work_group . push ( i , raw_client ) do |client |
310
+ regular_timeout = client . read_timeout
311
+ client . read_timeout = @config . slow_command_timeout > 0.0 ? @config . slow_command_timeout : regular_timeout
312
+ reply = client . call ( 'CLUSTER' , 'NODES' )
313
+ client . read_timeout = regular_timeout
314
+ parse_cluster_node_reply ( reply )
315
+ rescue StandardError => e
316
+ e
317
+ ensure
318
+ client &.close
319
+ end
320
+ end
321
+
322
+ node_info_list = errors = nil
323
+
324
+ work_group . each do |i , v |
325
+ case v
326
+ when StandardError
327
+ errors ||= Array . new ( startup_size )
328
+ errors [ i ] = v
329
+ else
330
+ node_info_list ||= Array . new ( startup_size )
331
+ node_info_list [ i ] = v
332
+ end
333
+ end
334
+
335
+ work_group . close
336
+
337
+ raise ::RedisClient ::Cluster ::InitialSetupError , errors if node_info_list . nil?
338
+
339
+ grouped = node_info_list . compact . group_by do |info_list |
340
+ info_list . sort_by! ( &:id )
341
+ info_list . each_with_object ( String . new ( capacity : 128 * info_list . size ) ) do |e , a |
342
+ a << e . id << e . node_key << e . role << e . primary_id << e . config_epoch
343
+ end
344
+ end
345
+
346
+ grouped . max_by { |_ , v | v . size } [ 1 ] . first
347
+ end
348
+
349
+ def parse_cluster_node_reply ( reply ) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
350
+ reply . each_line ( "\n " , chomp : true ) . filter_map do |line |
351
+ fields = line . split
352
+ flags = fields [ 2 ] . split ( ',' )
353
+ next unless fields [ 7 ] == 'connected' && ( flags & DEAD_FLAGS ) . empty?
354
+
355
+ slots = if fields [ 8 ] . nil?
356
+ EMPTY_ARRAY
357
+ else
358
+ fields [ 8 ..] . reject { |str | str . start_with? ( '[' ) }
359
+ . map { |str | str . split ( '-' ) . map { |s | Integer ( s ) } }
360
+ . map { |a | a . size == 1 ? a << a . first : a }
361
+ . map ( &:sort )
362
+ end
363
+
364
+ ::RedisClient ::Cluster ::Node ::Info . new (
365
+ id : fields [ 0 ] ,
366
+ node_key : parse_node_key ( fields [ 1 ] ) ,
367
+ role : ( flags & ROLE_FLAGS ) . first ,
368
+ primary_id : fields [ 3 ] ,
369
+ ping_sent : fields [ 4 ] ,
370
+ pong_recv : fields [ 5 ] ,
371
+ config_epoch : fields [ 6 ] ,
372
+ link_state : fields [ 7 ] ,
373
+ slots : slots
374
+ )
375
+ end
376
+ end
377
+
378
+ # As redirection node_key is dependent on `cluster-preferred-endpoint-type` config,
379
+ # node_key should use hostname if present in CLUSTER NODES output.
380
+ #
381
+ # See https://redis.io/commands/cluster-nodes/ for details on the output format.
382
+ # node_address matches fhe format: <ip:port@cport[,hostname[,auxiliary_field=value]*]>
383
+ def parse_node_key ( node_address )
384
+ ip_chunk , hostname , _auxiliaries = node_address . split ( ',' )
385
+ ip_port_string = ip_chunk . split ( '@' ) . first
386
+ return ip_port_string if hostname . nil? || hostname . empty?
387
+
388
+ port = ip_port_string . split ( ':' ) [ 1 ]
389
+ "#{ hostname } :#{ port } "
390
+ end
391
+
392
+ def with_startup_clients ( count ) # rubocop:disable Metrics/AbcSize
393
+ if @config . connect_with_original_config
394
+ # If connect_with_original_config is set, that means we need to build actual client objects
395
+ # and close them, so that we e.g. re-resolve a DNS entry with the cluster nodes in it.
396
+ begin
397
+ # Memoize the startup clients, so we maintain RedisClient's internal circuit breaker configuration
398
+ # if it's set.
399
+ @startup_clients ||= @config . startup_nodes . values . sample ( count ) . map do |node_config |
400
+ ::RedisClient ::Cluster ::Node ::Config . new ( **node_config ) . new_client
401
+ end
402
+ yield @startup_clients
403
+ ensure
404
+ # Close the startup clients when we're done, so we don't maintain pointless open connections to
405
+ # the cluster though
406
+ @startup_clients &.each ( &:close )
407
+ end
408
+ else
409
+ # (re-)connect using nodes we already know about.
410
+ # If this is the first time we're connecting to the cluster, we need to seed the topology with the
411
+ # startup clients though.
412
+ @topology . process_topology_update! ( { } , @config . startup_nodes ) if @topology . clients . empty?
413
+ yield @topology . clients . values . sample ( count )
414
+ end
415
+ end
416
+
417
+ def with_reload_lock
418
+ # What should happen with concurrent calls #reload? This is a realistic possibility if the cluster goes into
419
+ # a CLUSTERDOWN state, and we're using a pooled backend. Every thread will independently discover this, and
420
+ # call reload!.
421
+ # For now, if a reload is in progress, wait for that to complete, and consider that the same as us having
422
+ # performed the reload.
423
+ # Probably in the future we should add a circuit breaker to #reload itself, and stop trying if the cluster is
424
+ # obviously not working.
425
+ wait_start = Process . clock_gettime ( Process ::CLOCK_MONOTONIC )
426
+ @mutex . synchronize do
427
+ return if @last_reloaded_at && @last_reloaded_at > wait_start
428
+
429
+ r = yield
430
+ @last_reloaded_at = Process . clock_gettime ( Process ::CLOCK_MONOTONIC )
431
+ r
432
+ end
433
+ end
381
434
end
382
435
end
383
436
end
0 commit comments