6060import java .nio .file .StandardCopyOption ;
6161import java .util .Arrays ;
6262import java .util .Comparator ;
63+ import java .util .List ;
64+ import java .util .concurrent .CompletableFuture ;
6365import java .util .concurrent .ExecutorService ;
6466import java .util .concurrent .ScheduledExecutorService ;
6567import java .util .concurrent .TimeUnit ;
@@ -71,16 +73,40 @@ public class ConfigRegionStateMachine implements IStateMachine, IStateMachine.Ev
7173
7274 private static final Logger LOGGER = LoggerFactory .getLogger (ConfigRegionStateMachine .class );
7375
74- private static final ExecutorService threadPool =
76+ /**
77+ * Serializes leadership transitions (become-leader / resign-leader). A single worker thread is
78+ * the barrier that keeps epochs strictly serial: the orchestration of one transition runs to
79+ * completion before the next one begins.
80+ */
81+ private static final ExecutorService leaderServicesTransitionExecutor =
82+ IoTDBThreadPoolFactory .newSingleThreadExecutor (
83+ ThreadName .CONFIG_NODE_LEADER_SERVICES_TRANSITION .getName ());
84+
85+ /** Runs the individual leader services in parallel within a single become-leader epoch. */
86+ private static final ExecutorService leaderServicesStartupPool =
7587 IoTDBThreadPoolFactory .newCachedThreadPool (ThreadName .CONFIG_NODE_RECOVER .getName ());
88+
7689 private static final ConfigNodeConfig CONF = ConfigNodeDescriptor .getInstance ().getConf ();
7790 private static final long WAIT_LOAD_READY_TIMEOUT_MS =
7891 CommonDescriptor .getInstance ().getConfig ().getCnConnectionTimeoutInMS () / 2 ;
7992 private static final long WAIT_LOAD_READY_INTERVAL_MS = 100 ;
8093 private final ConfigPlanExecutor executor ;
94+
95+ /**
96+ * Whether the leader services of the {@link #leaderServicesEpoch current epoch} have finished
97+ * starting up. Read by {@link ConsensusManager#confirmLeader()} to gate external serving.
98+ */
8199 private final AtomicBoolean leaderServicesReady ;
100+
101+ /**
102+ * Monotonically increasing leadership generation. Every become-leader / resign-leader transition
103+ * bumps it, so any work submitted for an older epoch can detect it is stale and bail out.
104+ */
82105 private final AtomicLong leaderServicesEpoch ;
106+
107+ /** Guards {@link #leaderServicesReady} and {@link #leaderServicesEpoch} as a unit. */
83108 private final Object leaderServicesLock ;
109+
84110 private ConfigManager configManager ;
85111
86112 /** Variables for {@link ConfigNode} Simple Consensus. */
@@ -241,85 +267,92 @@ public void loadSnapshot(final File latestSnapshotRootDir) {
241267 public void notifyLeaderChanged (ConsensusGroupId groupId , int newLeaderId ) {
242268 // We get currentNodeId here because the currentNodeId
243269 // couldn't initialize earlier than the ConfigRegionStateMachine
244- int currentNodeId = ConfigNodeDescriptor .getInstance ().getConf ().getConfigNodeId ();
270+ final int currentNodeId = ConfigNodeDescriptor .getInstance ().getConf ().getConfigNodeId ();
245271 if (currentNodeId != newLeaderId ) {
246- invalidateLeaderServices ();
247272 LOGGER .info (
248273 ConfigNodeMessages .CURRENT_NODE_NODEID_IP_PORT_IS_NO_LONGER_THE_LEADER
249274 + "the new leader is [nodeId:{}]" ,
250275 currentNodeId ,
251276 currentNodeTEndPoint ,
252277 newLeaderId );
278+ resignLeaderAsync ();
253279 }
254280 }
255281
256282 @ Override
257283 public void notifyNotLeader () {
258- invalidateLeaderServices ();
259284 // We get currentNodeId here because the currentNodeId
260285 // couldn't initialize earlier than the ConfigRegionStateMachine
261- int currentNodeId = ConfigNodeDescriptor .getInstance ().getConf ().getConfigNodeId ();
286+ final int currentNodeId = ConfigNodeDescriptor .getInstance ().getConf ().getConfigNodeId ();
262287 LOGGER .info (
263288 ConfigNodeMessages .CURRENT_NODE_NODEID_IP_PORT_IS_NO_LONGER_THE_LEADER
264289 + "start cleaning up related services" ,
265290 currentNodeId ,
266291 currentNodeTEndPoint );
267- synchronized (leaderServicesLock ) {
268- // Stop leader scheduling services
269- configManager .getPipeManager ().getPipeRuntimeCoordinator ().stopPipeMetaSync ();
270- configManager .getPipeManager ().getPipeRuntimeCoordinator ().stopPipeHeartbeat ();
271- configManager
272- .getSubscriptionManager ()
273- .getSubscriptionCoordinator ()
274- .stopSubscriptionMetaSync ();
275- configManager .getLoadManager ().stopTopologyService ();
276- configManager .getLoadManager ().stopLoadServices ();
277- configManager .getProcedureManager ().stopExecutor ();
278- configManager .getRetryFailedTasksThread ().stopRetryFailedTasksService ();
279- configManager .getPartitionManager ().stopRegionCleaner ();
280- configManager .getCQManager ().stopCQScheduler ();
281- configManager .getClusterSchemaManager ().clearSchemaQuotaCache ();
282- // Remove Metric after leader change
283- configManager .removeMetrics ();
284-
285- // Shutdown leader related service for config pipe
286- PipeConfigNodeAgent .runtime ().notifyLeaderUnavailable ();
287-
288- // Clean receiver file dir
289- PipeConfigNodeAgent .receiver ().cleanPipeReceiverDir ();
290- }
291-
292- LOGGER .info (
293- ConfigNodeMessages .CURRENT_NODE_NODEID_IP_PORT_IS_NO_LONGER_THE_LEADER
294- + "all services on old leader are unavailable now." ,
295- currentNodeId ,
296- currentNodeTEndPoint );
292+ resignLeaderAsync ();
297293 }
298294
299295 @ Override
300296 public void notifyLeaderReady () {
301- final long epoch = nextLeaderServicesEpoch ();
302297 LOGGER .info (
303298 ConfigNodeMessages .CURRENT_NODE_NODEID_IP_PORT_BECOMES_CONFIG_REGION_LEADER ,
304299 ConfigNodeDescriptor .getInstance ().getConf ().getConfigNodeId (),
305300 currentNodeTEndPoint );
301+ // Bump the epoch eagerly so that any in-flight services of an older epoch are invalidated
302+ // immediately, even before the (serialized) become-leader orchestration gets to run.
303+ final long epoch = nextLeaderServicesEpoch ();
304+ leaderServicesTransitionExecutor .submit (() -> becomeLeader (epoch ));
305+ }
306+
307+ /**
308+ * Submit a resign-leader transition. The epoch is bumped eagerly (on the consensus thread) so
309+ * that stale leader work is invalidated at once, while the teardown itself is serialized behind
310+ * any in-flight transition on {@link #leaderServicesTransitionExecutor}.
311+ */
312+ private void resignLeaderAsync () {
313+ invalidateLeaderServices ();
314+ leaderServicesTransitionExecutor .submit (this ::stopLeaderServices );
315+ }
316+
317+ /**
318+ * Bring up the leader services for {@code epoch}. Runs on the single transition thread, so it is
319+ * strictly serialized against every other transition. Within the epoch, the load services start
320+ * first (to warm up as early as possible), then the remaining services start in parallel and are
321+ * joined before the epoch is marked ready.
322+ */
323+ private void becomeLeader (final long epoch ) {
324+ if (!isCurrentLeaderServicesEpoch (epoch )) {
325+ LOGGER .info (
326+ ConfigNodeMessages .CURRENT_NODE_NODEID_IP_PORT_IS_NO_LONGER_THE_LEADER
327+ + "skip starting leader services because the leader epoch is stale" ,
328+ ConfigNodeDescriptor .getInstance ().getConf ().getConfigNodeId (),
329+ currentNodeTEndPoint );
330+ return ;
331+ }
306332
307333 // Always start load services first. ConsensusManager gates external serving until warm-up.
308334 configManager .getLoadManager ().startLoadServices ();
309-
310335 if (CONF .isEnableTopologyProbing ()) {
311336 configManager .getLoadManager ().startTopologyService ();
312337 }
313338
314- threadPool .submit (() -> startLeaderServicesAndWaitForLoadReady (epoch ));
315- }
339+ // Start the remaining leader services in parallel and wait for all of them to finish.
340+ final CompletableFuture <?>[] startups =
341+ leaderServiceStartups ().stream ()
342+ .map (startup -> startInParallelIfEpochCurrent (epoch , startup ))
343+ .toArray (CompletableFuture []::new );
344+ CompletableFuture .allOf (startups ).join ();
316345
317- private void startLeaderServicesAndWaitForLoadReady (final long epoch ) {
318- if (!startLeaderServices (epoch )) {
346+ if (!isCurrentLeaderServicesEpoch (epoch )) {
319347 return ;
320348 }
349+ // The procedure executor may report readiness asynchronously once it has caught up.
350+ configManager
351+ .getProcedureManager ()
352+ .startExecutor (() -> markLeaderServicesReadyIfEpochCurrent (epoch ));
353+ markLeaderServicesReadyIfEpochCurrent (epoch );
321354
322- boolean loadReady = waitForLoadReady (epoch );
355+ final boolean loadReady = waitForLoadReady (epoch );
323356 if (!isCurrentLeaderServicesEpoch (epoch )) {
324357 return ;
325358 }
@@ -330,75 +363,85 @@ private void startLeaderServicesAndWaitForLoadReady(final long epoch) {
330363 currentNodeTEndPoint );
331364 }
332365
333- private boolean startLeaderServices (final long epoch ) {
334- synchronized (leaderServicesLock ) {
335- if (!isCurrentLeaderServicesEpoch (epoch )) {
336- LOGGER .info (
337- ConfigNodeMessages .CURRENT_NODE_NODEID_IP_PORT_IS_NO_LONGER_THE_LEADER
338- + "skip starting leader services because the leader epoch is stale" ,
339- ConfigNodeDescriptor .getInstance ().getConf ().getConfigNodeId (),
340- currentNodeTEndPoint );
341- return false ;
342- }
366+ /** The leader services that can be started independently, in parallel, within one epoch. */
367+ private List <Runnable > leaderServiceStartups () {
368+ return Arrays .asList (
369+ () -> configManager .getProcedureManager ().getStore ().getProcedureInfo ().upgrade (),
370+ () -> configManager .getRetryFailedTasksThread ().startRetryFailedTasksService (),
371+ () -> configManager .getPartitionManager ().startRegionCleaner (),
372+ // Add metrics after leader ready.
373+ () -> configManager .addMetrics (),
374+ // Activate leader related service for config pipe.
375+ () -> PipeConfigNodeAgent .runtime ().notifyLeaderReady (),
376+ // CQ recovery may be time-consuming, so it is just one more parallel startup.
377+ () -> configManager .getCQManager ().startCQScheduler (),
378+ () -> configManager .getPipeManager ().getPipeRuntimeCoordinator ().startPipeMetaSync (),
379+ () -> configManager .getPipeManager ().getPipeRuntimeCoordinator ().startPipeHeartbeat (),
380+ () ->
381+ configManager
382+ .getPipeManager ()
383+ .getPipeRuntimeCoordinator ()
384+ .onConfigRegionGroupLeaderChanged (),
385+ () ->
386+ configManager
387+ .getSubscriptionManager ()
388+ .getSubscriptionCoordinator ()
389+ .startSubscriptionMetaSync (),
390+ // To adapt old version, we check cluster ID after state machine has been fully recovered.
391+ () -> configManager .getClusterManager ().checkClusterId ());
392+ }
393+
394+ /** Tear down every leader service. Runs on the single transition thread. */
395+ private void stopLeaderServices () {
396+ final int currentNodeId = ConfigNodeDescriptor .getInstance ().getConf ().getConfigNodeId ();
397+ // Stop leader scheduling services
398+ configManager .getPipeManager ().getPipeRuntimeCoordinator ().stopPipeMetaSync ();
399+ configManager .getPipeManager ().getPipeRuntimeCoordinator ().stopPipeHeartbeat ();
400+ configManager .getSubscriptionManager ().getSubscriptionCoordinator ().stopSubscriptionMetaSync ();
401+ configManager .getLoadManager ().stopTopologyService ();
402+ configManager .getLoadManager ().stopLoadServices ();
403+ configManager .getProcedureManager ().stopExecutor ();
404+ configManager .getRetryFailedTasksThread ().stopRetryFailedTasksService ();
405+ configManager .getPartitionManager ().stopRegionCleaner ();
406+ configManager .getCQManager ().stopCQScheduler ();
407+ configManager .getClusterSchemaManager ().clearSchemaQuotaCache ();
408+ // Remove Metric after leader change
409+ configManager .removeMetrics ();
343410
344- // Start leader scheduling services
345- submitIfLeaderServicesEpochCurrent (
346- epoch , () -> configManager .getProcedureManager ().getStore ().getProcedureInfo ().upgrade ());
347- configManager .getRetryFailedTasksThread ().startRetryFailedTasksService ();
348- configManager .getPartitionManager ().startRegionCleaner ();
349- // Add Metric after leader ready
350- configManager .addMetrics ();
351-
352- // Activate leader related service for config pipe
353- PipeConfigNodeAgent .runtime ().notifyLeaderReady ();
354-
355- // we do cq recovery async for performance:
356- // cq recovery may be time-consuming, we use another thread to do it in
357- // make notifyLeaderChanged not blocked by it
358- submitIfLeaderServicesEpochCurrent (
359- epoch , () -> configManager .getCQManager ().startCQScheduler ());
360-
361- submitIfLeaderServicesEpochCurrent (
362- epoch ,
363- () -> configManager .getPipeManager ().getPipeRuntimeCoordinator ().startPipeMetaSync ());
364- submitIfLeaderServicesEpochCurrent (
365- epoch ,
366- () -> configManager .getPipeManager ().getPipeRuntimeCoordinator ().startPipeHeartbeat ());
367- submitIfLeaderServicesEpochCurrent (
368- epoch ,
369- () ->
370- configManager
371- .getPipeManager ()
372- .getPipeRuntimeCoordinator ()
373- .onConfigRegionGroupLeaderChanged ());
374-
375- submitIfLeaderServicesEpochCurrent (
376- epoch ,
377- () ->
378- configManager
379- .getSubscriptionManager ()
380- .getSubscriptionCoordinator ()
381- .startSubscriptionMetaSync ());
382-
383- // To adapt old version, we check cluster ID after state machine has been fully recovered.
384- // Do check async because sync will be slow and block every other things.
385- submitIfLeaderServicesEpochCurrent (
386- epoch , () -> configManager .getClusterManager ().checkClusterId ());
387-
388- if (!isCurrentLeaderServicesEpoch (epoch )) {
389- return false ;
390- }
391- configManager
392- .getProcedureManager ()
393- .startExecutor (() -> markLeaderServicesReadyIfEpochCurrent (epoch ));
394- markLeaderServicesReadyIfEpochCurrent (epoch );
395- return leaderServicesReady .get ();
396- }
411+ // Shutdown leader related service for config pipe
412+ PipeConfigNodeAgent .runtime ().notifyLeaderUnavailable ();
413+
414+ // Clean receiver file dir
415+ PipeConfigNodeAgent .receiver ().cleanPipeReceiverDir ();
416+
417+ LOGGER .info (
418+ ConfigNodeMessages .CURRENT_NODE_NODEID_IP_PORT_IS_NO_LONGER_THE_LEADER
419+ + "all services on old leader are unavailable now." ,
420+ currentNodeId ,
421+ currentNodeTEndPoint );
422+ }
423+
424+ /**
425+ * Run {@code startup} on {@link #leaderServicesStartupPool}, skipping it (and recording success)
426+ * if the epoch has gone stale by the time it is picked up. Returns a future that always completes
427+ * normally so {@link CompletableFuture#allOf} acts as a clean join barrier.
428+ */
429+ private CompletableFuture <Void > startInParallelIfEpochCurrent (
430+ final long epoch , final Runnable startup ) {
431+ return CompletableFuture .runAsync (
432+ () -> {
433+ if (isCurrentLeaderServicesEpoch (epoch )) {
434+ startup .run ();
435+ }
436+ },
437+ leaderServicesStartupPool );
397438 }
398439
399440 private void markLeaderServicesReadyIfEpochCurrent (final long epoch ) {
400- if (isCurrentLeaderServicesEpoch (epoch )) {
401- leaderServicesReady .set (true );
441+ synchronized (leaderServicesLock ) {
442+ if (isCurrentLeaderServicesEpoch (epoch )) {
443+ leaderServicesReady .set (true );
444+ }
402445 }
403446 }
404447
@@ -442,30 +485,27 @@ public boolean areLeaderServicesReady() {
442485 return leaderServicesReady .get ();
443486 }
444487
488+ /** Open a new leadership generation, invalidating the previous one. */
445489 private long nextLeaderServicesEpoch () {
446- leaderServicesReady .set (false );
447- return leaderServicesEpoch .incrementAndGet ();
490+ synchronized (leaderServicesLock ) {
491+ leaderServicesReady .set (false );
492+ return leaderServicesEpoch .incrementAndGet ();
493+ }
448494 }
449495
496+ /** Invalidate the current leadership generation without opening a serving one. */
450497 private void invalidateLeaderServices () {
451- leaderServicesReady .set (false );
452- leaderServicesEpoch .incrementAndGet ();
498+ synchronized (leaderServicesLock ) {
499+ leaderServicesReady .set (false );
500+ leaderServicesEpoch .incrementAndGet ();
501+ }
453502 }
454503
455504 private boolean isCurrentLeaderServicesEpoch (final long epoch ) {
456505 return leaderServicesEpoch .get () == epoch
457506 && configManager .getConsensusManager ().isLeaderReady ();
458507 }
459508
460- private void submitIfLeaderServicesEpochCurrent (final long epoch , final Runnable task ) {
461- threadPool .submit (
462- () -> {
463- if (isCurrentLeaderServicesEpoch (epoch )) {
464- task .run ();
465- }
466- });
467- }
468-
469509 @ Override
470510 public void start () {
471511 if (ConsensusFactory .SIMPLE_CONSENSUS .equals (CONF .getConfigNodeConsensusProtocolClass ())) {
0 commit comments