Skip to content

Commit c75e9a8

Browse files
authored
Prohibit rollback of a procedure which has passed its commit point (#18909)
1 parent 85277b3 commit c75e9a8

17 files changed

Lines changed: 500 additions & 8 deletions

File tree

‎iotdb-core/confignode/src/main/i18n/en/org/apache/iotdb/confignode/i18n/ProcedureMessages.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1439,4 +1439,11 @@ private ProcedureMessages() {}
14391439
"DataNode setSystemStatus RPC failed: %s: %s: %s; ";
14401440
public static final String EXCEPTION_CONFIGNODE_TRYSETNODESTATUS_FAILED_ARG_ARG_ARG_C9BA78F4 =
14411441
"ConfigNode trySetNodeStatus failed: %s: %s: %s; ";
1442+
public static final String
1443+
LOG_PROCEDURE_ARG_IS_PAST_ITS_COMMIT_POINT_KEEPING_EVERY_STATE_IT_EXECUTED_CA21D7E9 =
1444+
"Procedure {} is past its commit point; keeping every state it executed.";
1445+
public static final String LOG_FINISH_PROCEDURE_WITHOUT_ROLLBACK_41F7A354 =
1446+
"finish procedure without rollback";
1447+
public static final String LOG_DELETE_CHILD_PROCEDURES_WITHOUT_ROLLBACK_6E62D1C1 =
1448+
"delete child procedures without rollback";
14421449
}

‎iotdb-core/confignode/src/main/i18n/zh/org/apache/iotdb/confignode/i18n/ProcedureMessages.java‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1524,4 +1524,10 @@ private ProcedureMessages() {}
15241524
"DataNode setSystemStatus RPC 失败:%s:%s:%s; ";
15251525
public static final String EXCEPTION_CONFIGNODE_TRYSETNODESTATUS_FAILED_ARG_ARG_ARG_C9BA78F4 =
15261526
"ConfigNode trySetNodeStatus 失败:%s:%s:%s; ";
1527+
public static final String
1528+
LOG_PROCEDURE_ARG_IS_PAST_ITS_COMMIT_POINT_KEEPING_EVERY_STATE_IT_EXECUTED_CA21D7E9 =
1529+
"流程 {} 已越过提交点,保留它执行过的所有状态。";
1530+
public static final String LOG_FINISH_PROCEDURE_WITHOUT_ROLLBACK_41F7A354 = "结束流程(不回滚)";
1531+
public static final String LOG_DELETE_CHILD_PROCEDURES_WITHOUT_ROLLBACK_6E62D1C1 =
1532+
"删除子流程(不回滚)";
15271533
}

‎iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/Procedure.java‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,24 @@ public final boolean hasLock() {
9999
protected abstract void rollback(Env env)
100100
throws IOException, InterruptedException, ProcedureException;
101101

102+
/**
103+
* Whether the states this procedure has already executed may still be undone. The executor asks
104+
* this once, before it starts to roll back the root procedure stack. A procedure which returns
105+
* false is finished without undoing any of its states, see {@link #abandonRollback()}.
106+
*/
107+
protected boolean isRollbackSupported() {
108+
return true;
109+
}
110+
111+
/**
112+
* Drops the bookkeeping of the states this procedure has executed. The persisted stack indexes
113+
* are what a recovered rollback rebuilds its stack from, so dropping them is what makes the
114+
* decision survive a leader switch.
115+
*/
116+
protected synchronized void abandonRollback() {
117+
stackIndexes = null;
118+
}
119+
102120
public void serialize(DataOutputStream stream) throws IOException {
103121
// procid
104122
stream.writeLong(this.procId);

‎iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/ProcedureExecutor.java‎

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@
4040
import java.util.ArrayDeque;
4141
import java.util.ArrayList;
4242
import java.util.Arrays;
43+
import java.util.Collections;
4344
import java.util.Deque;
4445
import java.util.HashSet;
46+
import java.util.IdentityHashMap;
4547
import java.util.List;
4648
import java.util.Set;
4749
import java.util.concurrent.ConcurrentHashMap;
@@ -370,7 +372,7 @@ public boolean removeInternalProcedure(InternalProcedure internalProcedure) {
370372
*
371373
* @param proc procedure
372374
*/
373-
private void executeProcedure(Procedure<Env> proc) {
375+
private void executeProcedure(Procedure<Env> proc) throws InterruptedException {
374376
if (proc.isFinished()) {
375377
LOG.debug(ProcedureMessages.IS_ALREADY_FINISHED, proc);
376378
return;
@@ -689,7 +691,7 @@ private void yieldProcedure(Procedure<Env> proc) {
689691
* @return lock state
690692
*/
691693
private LockStateResult<Env> executeRootStackRollback(
692-
Long rootProcId, RootProcedureStack<Env> procedureStack) {
694+
Long rootProcId, RootProcedureStack<Env> procedureStack) throws InterruptedException {
693695
Procedure<Env> rootProcedure = procedures.get(rootProcId);
694696
ProcedureException exception = rootProcedure.getException();
695697
if (exception == null) {
@@ -700,14 +702,33 @@ private LockStateResult<Env> executeRootStackRollback(
700702
() -> store.update(rootProcedure), "root procedure rollback");
701703
}
702704
List<Procedure<Env>> subprocStack = procedureStack.getSubproceduresStack();
705+
// Decide which procedures must keep the states they have executed before anything is rolled
706+
// back: the decision reads the state history, which the rollback itself shrinks. Identity is
707+
// used because procedures override equals with the data of the operation they run.
708+
final Set<Procedure<Env>> proceduresKeepingTheirStates =
709+
Collections.newSetFromMap(new IdentityHashMap<>());
710+
for (Procedure<Env> procedure : subprocStack) {
711+
// A null slot is a procedure whose record was deleted before the snapshot this stack was
712+
// rebuilt from, so there is nothing left to roll back for it.
713+
if (procedure != null && !procedure.isSuccess() && !procedure.isRollbackSupported()) {
714+
proceduresKeepingTheirStates.add(procedure);
715+
}
716+
}
703717
int stackTail = subprocStack.size();
704718
while (stackTail-- > 0) {
705719
Procedure<Env> procedure = subprocStack.get(stackTail);
720+
if (procedure == null) {
721+
continue;
722+
}
706723
if (procedure.isSuccess()) {
707724
subprocStack.remove(stackTail);
708725
cleanupAfterRollback(procedure);
709726
continue;
710727
}
728+
if (proceduresKeepingTheirStates.contains(procedure)) {
729+
// Finished below, once the rest of the stack has been rolled back.
730+
continue;
731+
}
711732
ProcedureLockState lockState = acquireLock(procedure);
712733
if (lockState != ProcedureLockState.LOCK_ACQUIRED) {
713734
return new LockStateResult<>(lockState, procedure);
@@ -730,12 +751,109 @@ private LockStateResult<Env> executeRootStackRollback(
730751
}
731752
}
732753

754+
if (!proceduresKeepingTheirStates.isEmpty()) {
755+
// The children go first: releasing a child's lock writes its record back, which has to be
756+
// covered by the root's cleanup. The root is finished last so that no write of a child
757+
// follows the write which persists the root as finished.
758+
for (Procedure<Env> procedure : proceduresKeepingTheirStates) {
759+
if (procedure.hasParent()) {
760+
finishWithoutRollback(procedure);
761+
}
762+
}
763+
for (Procedure<Env> procedure : proceduresKeepingTheirStates) {
764+
if (!procedure.hasParent()) {
765+
finishWithoutRollback(procedure);
766+
}
767+
}
768+
subprocStack.removeIf(proceduresKeepingTheirStates::contains);
769+
}
770+
733771
LOG.info(
734772
ProcedureMessages.ROLLED_BACK_TIME_DURATION_IS, rootProcedure, rootProcedure.elapsedTime());
735773
rootProcedureCleanup(rootProcedure);
736774
return new LockStateResult<>(ProcedureLockState.LOCK_ACQUIRED, rootProcedure);
737775
}
738776

777+
/**
778+
* Finishes a procedure without undoing any of the states it has executed. The failure is still
779+
* reported, as the operation the procedure was part of did not complete; what is dropped is only
780+
* the bookkeeping of the rollback, so that neither this rollback nor the one a new leader
781+
* recovers touches this procedure again.
782+
*/
783+
private void finishWithoutRollback(final Procedure<Env> procedure) throws InterruptedException {
784+
// The unlock persists the procedure through doReleaseLock(). It is done first, while the
785+
// procedure is neither marked finished nor stripped of its state history, so that a snapshot
786+
// taken right after this write is still a state recovery resumes. Forcing the release matters
787+
// because the procedure is not finished yet, so releaseLock() would otherwise keep the lock of
788+
// procedures which hold it across steps.
789+
releaseLockIfHeld(procedure);
790+
if (procedure.hasParent()) {
791+
// Mark the procedure finished before dropping its state history, so that a worker which
792+
// picks it up concurrently returns at the isFinished() check instead of rolling it back.
793+
if (!procedure.isSuccess()) {
794+
procedure.setState(ProcedureState.ROLLEDBACK);
795+
}
796+
procedure.abandonRollback();
797+
try {
798+
store.delete(procedure.getProcId());
799+
// do not remove this procedure when exception occurred
800+
procedures.remove(procedure.getProcId());
801+
} catch (final Exception e) {
802+
LOG.warn(ProcedureMessages.FAILED_TO_DELETE_PROCEDURE_ON_ROLLBACK, e);
803+
}
804+
} else {
805+
// A child which never executed has no rollback step and is not visited above, so its record
806+
// goes with the ones of the children which were.
807+
// Every child record has to be gone before the root may be persisted as finished: a snapshot
808+
// holding a finished root beside a child record makes recover() fail on that child. Deleting
809+
// is idempotent, so it is retried until durable.
810+
final long[] childProcIds = rollbackStack.get(procedure.getProcId()).getSubprocedureIds();
811+
for (final long childProcId : childProcIds) {
812+
RetryUtils.executeWithEndlessBackoffRetry(
813+
() -> store.delete(childProcId),
814+
ProcedureMessages.LOG_DELETE_CHILD_PROCEDURES_WITHOUT_ROLLBACK_6E62D1C1);
815+
// The retry helper also returns on interruption. Abort the entire rollback before
816+
// marking the root finished or letting the caller clean it up.
817+
if (Thread.currentThread().isInterrupted()) {
818+
throw new InterruptedException();
819+
}
820+
}
821+
if (!procedure.isSuccess()) {
822+
procedure.setState(ProcedureState.ROLLEDBACK);
823+
}
824+
procedure.abandonRollback();
825+
// The stack indexes and the states just dropped are persisted here, so that the rollback
826+
// recovered by a new leader leaves this procedure out. Endless retry since this step is not
827+
// idempotent.
828+
RetryUtils.executeWithEndlessBackoffRetry(
829+
() -> store.update(procedure),
830+
ProcedureMessages.LOG_FINISH_PROCEDURE_WITHOUT_ROLLBACK_41F7A354);
831+
if (Thread.currentThread().isInterrupted()) {
832+
throw new InterruptedException();
833+
}
834+
}
835+
procedure.updateMetricsOnFinish(getEnvironment(), procedure.elapsedTime(), false);
836+
LOG.info(
837+
ProcedureMessages
838+
.LOG_PROCEDURE_ARG_IS_PAST_ITS_COMMIT_POINT_KEEPING_EVERY_STATE_IT_EXECUTED_CA21D7E9,
839+
procedure);
840+
}
841+
842+
/**
843+
* The rollback loop skipped acquireLock() for procedures finished without rollback, so a lock
844+
* restored by restoreLock() on load is still recorded in lockedWhenLoading instead of locked.
845+
* Finish that bookkeeping and release the lock here: rootProcedureCleanup() releases only the
846+
* locks that hasLock() reports, and a procedure finished without rollback must not keep holding
847+
* its lock.
848+
*/
849+
private void releaseLockIfHeld(final Procedure<Env> procedure) {
850+
if (procedure.hasLock() || procedure.isLockedWhenLoading()) {
851+
if (acquireLock(procedure) == ProcedureLockState.LOCK_ACQUIRED) {
852+
releaseLock(procedure, true);
853+
}
854+
}
855+
}
856+
739857
private ProcedureLockState acquireLock(Procedure<Env> proc) {
740858
if (proc.hasLock()) {
741859
return ProcedureLockState.LOCK_ACQUIRED;

‎iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/RootProcedureStack.java‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,9 @@ protected synchronized List<Procedure<Env>> getSubproceduresStack() {
8989
protected synchronized ProcedureException getException() {
9090
if (subprocStack != null) {
9191
for (Procedure<Env> proc : subprocStack) {
92-
if (proc.hasException()) {
92+
// Null slots come from procedures whose records were deleted before the stack was
93+
// rebuilt, see ProcedureExecutor#executeRootStackRollback.
94+
if (proc != null && proc.hasException()) {
9395
return proc.getException();
9496
}
9597
}

‎iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/StateMachineProcedure.java‎

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
import java.io.IOException;
3232
import java.nio.ByteBuffer;
3333
import java.util.ArrayList;
34+
import java.util.Iterator;
3435
import java.util.List;
3536
import java.util.concurrent.ConcurrentLinkedDeque;
3637

@@ -252,12 +253,49 @@ protected boolean isEofState() {
252253
return !states.isEmpty() && states.getLast() == EOF_STATE;
253254
}
254255

256+
@Override
257+
protected synchronized void abandonRollback() {
258+
super.abandonRollback();
259+
states.clear();
260+
}
261+
255262
/**
256-
* Used by the default implementation of abort() to know if the current state can be aborted and
257-
* rollback can be triggered.
263+
* Whether a failure in the specified state can be rolled back. A procedure asks this in its error
264+
* handling to tell a fatal failure from a retriable one; the executor asks it once, through the
265+
* overload without a state, to decide whether the procedure may be rolled back at all.
266+
*
267+
* <p>The executor asks with the state the procedure is at: the state which failed or is
268+
* scheduled, or the last state executed when every state has already run. A procedure which
269+
* returns false for the states after its commit point is therefore not rolled back once that
270+
* point has been passed.
258271
*/
259272
protected boolean isRollbackSupported(final TState state) {
260-
return false;
273+
return true;
274+
}
275+
276+
/**
277+
* Whether the states this procedure has executed may still be rolled back. The answer comes from
278+
* the state the procedure is at, through the per-state overload above.
279+
*/
280+
@Override
281+
protected final boolean isRollbackSupported() {
282+
return isRollbackSupported(getRollbackDecisionState());
283+
}
284+
285+
/**
286+
* The state a rollback decision is taken at. The last element of the history is the state which
287+
* failed or is scheduled; once every state has run, the end marker is last and the decision falls
288+
* on the state before it.
289+
*/
290+
private TState getRollbackDecisionState() {
291+
final Iterator<Integer> stateIds = states.descendingIterator();
292+
while (stateIds.hasNext()) {
293+
final int stateId = stateIds.next();
294+
if (stateId != EOF_STATE) {
295+
return getState(stateId);
296+
}
297+
}
298+
return getInitialState();
261299
}
262300

263301
private boolean noMoreState() {

‎iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/node/RemoveAINodeProcedure.java‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ protected void rollbackState(
135135
// no need to rollback
136136
}
137137

138+
/**
139+
* A failure in either state is retried instead of failing the procedure, which has nothing to
140+
* roll back.
141+
*/
142+
@Override
143+
protected boolean isRollbackSupported(final RemoveAINodeState state) {
144+
return false;
145+
}
146+
138147
@Override
139148
protected RemoveAINodeState getState(int stateId) {
140149
return RemoveAINodeState.values()[stateId];

‎iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AbstractSetPropertiesProcedure.java‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,23 @@ private void setProperties(final ConfigNodeProcedureEnv env) {
162162
protected abstract ConfigPhysicalPlan createSetPropertiesPlan(
163163
Map<String, String> properties, boolean isRollback);
164164

165+
/**
166+
* The properties are set on the ConfigNode once SET_PROPERTIES has completed, and the ConfigNode
167+
* schema is the source of truth the DataNodes resync from. The procedure therefore stays
168+
* rollbackable up to SET_PROPERTIES and not after it.
169+
*/
170+
@Override
171+
protected boolean isRollbackSupported(final SetTablePropertiesState state) {
172+
switch (state) {
173+
case VALIDATE_TABLE:
174+
case PRE_RELEASE:
175+
case SET_PROPERTIES:
176+
return true;
177+
default:
178+
return false;
179+
}
180+
}
181+
165182
@Override
166183
protected void rollbackState(
167184
final ConfigNodeProcedureEnv env, final SetTablePropertiesState state)

‎iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AddTableColumnProcedure.java‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,23 @@ protected String getActionMessage() {
154154
return "add table column";
155155
}
156156

157+
/**
158+
* The column is part of the table on the ConfigNode once ADD_COLUMN has completed, and the
159+
* ConfigNode schema is the source of truth the DataNodes resync from. The procedure therefore
160+
* stays rollbackable up to ADD_COLUMN and not after it.
161+
*/
162+
@Override
163+
protected boolean isRollbackSupported(final AddTableColumnState state) {
164+
switch (state) {
165+
case COLUMN_CHECK:
166+
case PRE_RELEASE:
167+
case ADD_COLUMN:
168+
return true;
169+
default:
170+
return false;
171+
}
172+
}
173+
157174
@Override
158175
protected void rollbackState(final ConfigNodeProcedureEnv env, final AddTableColumnState state)
159176
throws IOException, InterruptedException, ProcedureException {

‎iotdb-core/confignode/src/main/java/org/apache/iotdb/confignode/procedure/impl/schema/table/AlterTableColumnDataTypeProcedure.java‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ private void alterColumnDataType(final ConfigNodeProcedureEnv env) {
161161
setNextState(AlterTableColumnDataTypeState.COMMIT_RELEASE);
162162
}
163163

164+
/** The rollback of this procedure is a no-op, so it is never rolled back. */
164165
@Override
165166
protected boolean isRollbackSupported(final AlterTableColumnDataTypeState state) {
166167
return false;

0 commit comments

Comments
 (0)