-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Fix CQ recovery gap and stale callback contamination #17734
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -43,7 +43,10 @@ | |
|
|
||
| import java.util.Collections; | ||
| import java.util.List; | ||
| import java.util.concurrent.ConcurrentHashMap; | ||
| import java.util.concurrent.ConcurrentMap; | ||
| import java.util.concurrent.ScheduledExecutorService; | ||
| import java.util.concurrent.atomic.AtomicBoolean; | ||
| import java.util.concurrent.locks.ReadWriteLock; | ||
| import java.util.concurrent.locks.ReentrantReadWriteLock; | ||
|
|
||
|
|
@@ -57,11 +60,14 @@ | |
|
|
||
| private final ReadWriteLock lock; | ||
|
|
||
| private final ConcurrentMap<String, String> locallyScheduledCQs; | ||
|
|
||
| private ScheduledExecutorService executor; | ||
|
|
||
| public CQManager(ConfigManager configManager) { | ||
| this.configManager = configManager; | ||
| this.lock = new ReentrantReadWriteLock(); | ||
| this.locallyScheduledCQs = new ConcurrentHashMap<>(); | ||
| this.executor = | ||
| IoTDBThreadPoolFactory.newScheduledThreadPool( | ||
| CONF.getCqSubmitThread(), ThreadName.CQ_SCHEDULER.getName()); | ||
|
|
@@ -79,7 +85,11 @@ | |
|
|
||
| public TSStatus dropCQ(TDropCQReq req) { | ||
| try { | ||
| return configManager.getConsensusManager().write(new DropCQPlan(req.cqId)); | ||
| TSStatus status = configManager.getConsensusManager().write(new DropCQPlan(req.cqId)); | ||
| if (status.getCode() == TSStatusCode.SUCCESS_STATUS.getStatusCode()) { | ||
| locallyScheduledCQs.remove(req.cqId); | ||
| } | ||
| return status; | ||
|
Comment on lines
-82
to
+94
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dropping a CQ does not abort it?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in f09c3b0: dropCQ now coordinates with scheduler start/stop and cancels the local CQScheduleTask after the metadata drop succeeds, so queued future executions are aborted. |
||
| } catch (ConsensusException e) { | ||
| LOGGER.warn(ManagerMessages.UNEXPECTED_ERROR_HAPPENED_WHILE_DROPPING_CQ, req.cqId, e); | ||
| // consensus layer related errors | ||
|
|
@@ -113,7 +123,7 @@ | |
| return res; | ||
| } | ||
|
|
||
| public void startCQScheduler() { | ||
|
Check failure on line 126 in iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/cq/CQManager.java
|
||
| lock.writeLock().lock(); | ||
| try { | ||
| // 1. shutdown previous cq schedule thread pool | ||
|
|
@@ -132,6 +142,7 @@ | |
| executor = | ||
| IoTDBThreadPoolFactory.newScheduledThreadPool( | ||
| CONF.getCqSubmitThread(), ThreadName.CQ_SCHEDULER.getName()); | ||
| locallyScheduledCQs.clear(); | ||
|
|
||
| // 3. get all CQs | ||
| List<CQInfo.CQEntry> allCQs = null; | ||
|
|
@@ -155,8 +166,16 @@ | |
| if (allCQs != null) { | ||
| for (CQInfo.CQEntry entry : allCQs) { | ||
| if (entry.getState() == CQState.ACTIVE) { | ||
| if (!markCQLocallyScheduled(entry.getCqId(), entry.getMd5())) { | ||
| continue; | ||
| } | ||
| CQScheduleTask cqScheduleTask = new CQScheduleTask(entry, executor, configManager); | ||
| cqScheduleTask.submitSelf(); | ||
| try { | ||
| cqScheduleTask.submitSelf(); | ||
| } catch (RuntimeException e) { | ||
| unmarkCQLocallyScheduled(entry.getCqId(), entry.getMd5()); | ||
| throw e; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add the task as part of the value in locallyScheduledCQs?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in f09c3b0: locallyScheduledCQs now stores a LocallyScheduledCQ value containing the token and CQScheduleTask. CQScheduleTask owns its latest ScheduledFuture and exposes cancel(), so drop and token replacement can cancel it. |
||
| } | ||
| } | ||
| } | ||
|
|
@@ -176,11 +195,30 @@ | |
| try { | ||
| previous = executor; | ||
| executor = null; | ||
| locallyScheduledCQs.clear(); | ||
| } finally { | ||
| lock.writeLock().unlock(); | ||
| } | ||
| if (previous != null) { | ||
| previous.shutdown(); | ||
| } | ||
| } | ||
|
|
||
| public boolean markCQLocallyScheduled(String cqId, String md5) { | ||
|
Check warning on line 207 in iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/cq/CQManager.java
|
||
| AtomicBoolean shouldSchedule = new AtomicBoolean(false); | ||
| locallyScheduledCQs.compute( | ||
| cqId, | ||
| (ignored, previousMd5) -> { | ||
| if (!md5.equals(previousMd5)) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When previousMd5 is null, !md5.equals(previousMd5) schedules the CQ as expected. When previousMd5 equals md5, scheduling is skipped, which avoids duplicates. If the same cqId is updated with a new md5, this returns true and schedules again. Please confirm in this PR that any previously running CQScheduleTask for the old md5 is stopped or superseded; otherwise two tasks could run briefly.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in f09c3b0: the local schedule registry now stores the metadata token together with the CQScheduleTask. If the same CQ id is registered with a different token, the old task is cancelled before the new one is scheduled. Drop/start/stop also cancel local tasks, and cancelled callbacks no longer resubmit. |
||
| shouldSchedule.set(true); | ||
| return md5; | ||
| } | ||
| return previousMd5; | ||
| }); | ||
| return shouldSchedule.get(); | ||
| } | ||
|
|
||
| public void unmarkCQLocallyScheduled(String cqId, String md5) { | ||
|
Check warning on line 221 in iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/manager/cq/CQManager.java
|
||
| locallyScheduledCQs.remove(cqId, md5); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using remove(cqId, md5) ensures we only unmark the entry we own after a failed submitSelf. Good defensive pattern. |
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,11 +22,15 @@ | |
| import org.apache.iotdb.common.rpc.thrift.TSStatus; | ||
| import org.apache.iotdb.commons.exception.IoTDBException; | ||
| import org.apache.iotdb.commons.utils.ThriftCommonsSerDeUtils; | ||
| import org.apache.iotdb.confignode.consensus.request.read.cq.ShowCQPlan; | ||
| import org.apache.iotdb.confignode.consensus.request.write.cq.ActiveCQPlan; | ||
| import org.apache.iotdb.confignode.consensus.request.write.cq.AddCQPlan; | ||
| import org.apache.iotdb.confignode.consensus.request.write.cq.DropCQPlan; | ||
| import org.apache.iotdb.confignode.consensus.response.cq.ShowCQResp; | ||
| import org.apache.iotdb.confignode.i18n.ProcedureMessages; | ||
| import org.apache.iotdb.confignode.manager.cq.CQManager; | ||
| import org.apache.iotdb.confignode.manager.cq.CQScheduleTask; | ||
| import org.apache.iotdb.confignode.persistence.cq.CQInfo; | ||
| import org.apache.iotdb.confignode.procedure.env.ConfigNodeProcedureEnv; | ||
| import org.apache.iotdb.confignode.procedure.exception.ProcedureException; | ||
| import org.apache.iotdb.confignode.procedure.impl.node.AbstractNodeProcedure; | ||
|
|
@@ -45,6 +49,8 @@ | |
| import java.io.IOException; | ||
| import java.nio.ByteBuffer; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
| import java.util.UUID; | ||
| import java.util.concurrent.ScheduledExecutorService; | ||
|
|
||
| import static org.apache.iotdb.confignode.procedure.state.cq.CreateCQState.INACTIVE; | ||
|
|
@@ -75,7 +81,7 @@ | |
| public CreateCQProcedure(TCreateCQReq req, ScheduledExecutorService executor) { | ||
| super(); | ||
| this.req = req; | ||
| this.md5 = DigestUtils.md2Hex(req.cqId); | ||
| this.md5 = generateCQToken(req.cqId); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improper to call it md5 now.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in f09c3b0: renamed the CQ identifier from md5 to cqToken across the CQ plans, metadata, procedure and scheduler code. |
||
| this.executor = executor; | ||
| this.firstExecutionTime = | ||
| CQScheduleTask.getFirstExecutionTime(req.boundaryTime, req.everyInterval); | ||
|
|
@@ -91,12 +97,15 @@ | |
| addCQ(env); | ||
| return Flow.HAS_MORE_STATE; | ||
| case INACTIVE: | ||
| CQScheduleTask cqScheduleTask = | ||
| new CQScheduleTask(req, firstExecutionTime, md5, executor, env.getConfigManager()); | ||
| cqScheduleTask.submitSelf(); | ||
| submitScheduleTask( | ||
| env, | ||
| new CQScheduleTask(req, firstExecutionTime, md5, executor, env.getConfigManager())); | ||
| setNextState(SCHEDULED); | ||
| break; | ||
| case SCHEDULED: | ||
| if (isStateDeserialized()) { | ||
| recoverScheduledTask(env); | ||
| } | ||
| activeCQ(env); | ||
| return Flow.NO_MORE_STATE; | ||
| default: | ||
|
|
@@ -168,6 +177,42 @@ | |
| } | ||
| } | ||
|
|
||
| void recoverScheduledTask(ConfigNodeProcedureEnv env) throws ConsensusException { | ||
| Optional<CQInfo.CQEntry> cqEntry = getCurrentCQEntry(env); | ||
| if (!cqEntry.isPresent()) { | ||
| LOGGER.info( | ||
| "Skip recovering the schedule task of CQ {} because its metadata is unavailable.", | ||
| req.cqId); | ||
| return; | ||
| } | ||
| submitScheduleTask(env, new CQScheduleTask(cqEntry.get(), executor, env.getConfigManager())); | ||
| } | ||
|
|
||
| Optional<CQInfo.CQEntry> getCurrentCQEntry(ConfigNodeProcedureEnv env) throws ConsensusException { | ||
|
Check warning on line 191 in iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/cq/CreateCQProcedure.java
|
||
| ShowCQResp response = | ||
| (ShowCQResp) env.getConfigManager().getConsensusManager().read(new ShowCQPlan()); | ||
| return response.getCqList().stream() | ||
| .filter(entry -> req.cqId.equals(entry.getCqId()) && md5.equals(entry.getMd5())) | ||
| .findFirst(); | ||
| } | ||
|
Comment on lines
+191
to
+197
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to moderate ShowCQPlan to only return the desired CQs instead of filtering at the upper level?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in f09c3b0: ShowCQPlan now accepts an optional CQ id, and CQInfo filters by CQ id before returning the response. The recovery path now reads only the target CQ and then checks the token. |
||
|
|
||
| private static String generateCQToken(String cqId) { | ||
|
Check warning on line 199 in iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/cq/CreateCQProcedure.java
|
||
| return DigestUtils.md2Hex(cqId + "-" + UUID.randomUUID()); | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that you use UUID, it is no longer necessary to use md5.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in f09c3b0: generateCQToken now uses UUID.randomUUID().toString() directly and the DigestUtils/md2Hex dependency was removed from CreateCQProcedure. |
||
|
|
||
| private void submitScheduleTask(ConfigNodeProcedureEnv env, CQScheduleTask cqScheduleTask) { | ||
| CQManager cqManager = env.getConfigManager().getCQManager(); | ||
| if (!cqManager.markCQLocallyScheduled(req.cqId, md5)) { | ||
| return; | ||
| } | ||
| try { | ||
| cqScheduleTask.submitSelf(); | ||
| } catch (RuntimeException e) { | ||
| cqManager.unmarkCQLocallyScheduled(req.cqId, md5); | ||
| throw e; | ||
| } | ||
| } | ||
|
|
||
| @Override | ||
| protected void rollbackState(ConfigNodeProcedureEnv env, CreateCQState state) | ||
| throws IOException, InterruptedException, ProcedureException { | ||
|
|
@@ -272,4 +317,12 @@ | |
| md5, | ||
| firstExecutionTime); | ||
| } | ||
|
|
||
| public String getCqId() { | ||
| return req == null ? null : req.getCqId(); | ||
| } | ||
|
|
||
| public String getMd5() { | ||
| return md5; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Explain the key and the value.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in f09c3b0: added an inline explanation. The key is CQ id, and the value is the local task plus the metadata token it owns.