Skip to content

Commit cadcac4

Browse files
committed
Serialize ConfigNode leader services lifecycle
Refactor ConfigRegionStateMachine so leader become/resign transitions are strictly serial. All transitions (notifyLeaderReady / notifyNotLeader / notifyLeaderChanged) are submitted to a single-thread transition executor, which is the barrier that keeps epochs serial: one transition's orchestration runs to completion before the next begins. Within a become-leader epoch, load services start first to warm up as early as possible, then the remaining independent leader services start in parallel on a cached pool and are joined before the epoch is marked ready. The epoch is bumped eagerly on resign so an in-flight startup detects it is stale and bails out before re-enabling services after cleanup. This removes the giant lock-wrapped startLeaderServices method and the per-task submitIfLeaderServicesEpochCurrent helper; leaderServicesLock now only guards the (epoch, ready) pair.
1 parent 599a9be commit cadcac4

2 files changed

Lines changed: 162 additions & 121 deletions

File tree

iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/consensus/statemachine/ConfigRegionStateMachine.java

Lines changed: 160 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@
6060
import java.nio.file.StandardCopyOption;
6161
import java.util.Arrays;
6262
import java.util.Comparator;
63+
import java.util.List;
64+
import java.util.concurrent.CompletableFuture;
6365
import java.util.concurrent.ExecutorService;
6466
import java.util.concurrent.ScheduledExecutorService;
6567
import 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())) {

iotdb-core/node-commons/src/main/java/org/apache/iotdb/commons/concurrent/ThreadName.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public enum ThreadName {
9898
CONFIG_NODE_REGION_MAINTAINER("IoTDB-Region-Maintainer"),
9999
// -------------------------- ConfigNode-Recover --------------------------
100100
CONFIG_NODE_RECOVER("ConfigNode-Manager-Recovery"),
101+
CONFIG_NODE_LEADER_SERVICES_TRANSITION("ConfigNode-Leader-Services-Transition"),
101102
// -------------------------- ConfigNode-Procedure ------------------------
102103
// TODO: Use Thread Pool to manage the procedure thread @Potato
103104
CONFIG_NODE_PROCEDURE_WORKER("ProcedureWorkerGroup"),
@@ -374,7 +375,7 @@ public enum ThreadName {
374375
new HashSet<>(Arrays.asList(CONFIG_NODE_REGION_MAINTAINER));
375376

376377
private static final Set<ThreadName> configNodeRecoverThreadNames =
377-
new HashSet<>(Arrays.asList(CONFIG_NODE_RECOVER));
378+
new HashSet<>(Arrays.asList(CONFIG_NODE_RECOVER, CONFIG_NODE_LEADER_SERVICES_TRANSITION));
378379

379380
private static final Set<ThreadName> configNodeProcedureThreadNames =
380381
new HashSet<>(

0 commit comments

Comments
 (0)