4040import java .util .ArrayDeque ;
4141import java .util .ArrayList ;
4242import java .util .Arrays ;
43+ import java .util .Collections ;
4344import java .util .Deque ;
4445import java .util .HashSet ;
46+ import java .util .IdentityHashMap ;
4547import java .util .List ;
4648import java .util .Set ;
4749import 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 ;
0 commit comments