Yicong-Huang commented on code in PR #7122:
URL: https://github.com/apache/texera/pull/7122#discussion_r3755056591


##########
amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionExecutionManager.scala:
##########
@@ -196,39 +202,43 @@ class RegionExecutionManager(
       }.toSeq
 
     val endWorkerFuture: Future[Unit] =
-      Future.collect(endWorkerRequests).unit
+      Future
+        .collect(endWorkerRequests)
+        .within(killTimeout)
+        .unit
 
-    // 2. Send GracefulStops only after 1 has finished
+    // 2. Send GracefulStops with timeout
     val gracefulStopRequests: Future[Unit] =
       endWorkerFuture.flatMap { _ =>
         val gracefulStops =
           regionExecution.getAllOperatorExecutions.flatMap {
             case (_, opExec) =>
               opExec.getWorkerIds.map { workerId =>
                 val actorRef = actorRefService.getActorRef(workerId)
-                // Remove the actorRef so that no other actors can find the 
worker and send messages.
-                actorRefService.removeActorRef(workerId)
-                // Restarted regions reuse actorId. Remove stale control 
channels so the
-                // coordinator does not reuse old control-message sequence 
numbers for new workers.
-                asyncRPCClient.inputGateway.removeControlChannel(workerId)
-                asyncRPCClient.outputGateway.removeControlChannel(workerId)
                 gracefulStop(actorRef, ScalaDuration(5, 
TimeUnit.SECONDS)).asTwitter()
               }
           }.toSeq
 
-        Future.collect(gracefulStops).unit
+        Future
+          .collect(gracefulStops)
+          .within(killTimeout)
+          .unit
       }
 
-    // 3. Log whether the kills were successful
+    // 3. Cleanup only after all gracefulStops succeed
     gracefulStopRequests.transform {
       case Return(_) =>
         logger.debug(s"Region ${region.id.id} successfully terminated.")
+        val allWorkerIds = 
regionExecution.getAllOperatorExecutions.toSeq.flatMap {
+          case (_, opExec) => opExec.getWorkerIds
+        }
         regionExecution.getAllOperatorExecutions.foreach {
           case (_, opExec) =>
             opExec.getWorkerIds.foreach { workerId =>
               opExec.getWorkerExecution(workerId).forceTerminate()
             }
         }
+        actorService.self ! Coordinator.CleanupWorkerChannels(allWorkerIds)

Review Comment:
   This gets the mutation onto the right thread but drops the ordering the 
inline placement had. The send is fire-and-forget and `Future.Unit` follows 
immediately, so the termination future resolves while the message is still 
queued.
   
   Its continuation is `advanceRegionExecutions` 
(WorkflowExecutionManager.scala:110-117), running on the global EC. That 
rebuilds the region with the same worker ids (:140-153) and sends control RPCs, 
which can take the old sequence counter while the fresh worker's channel starts 
at 0. The late cleanup then renumbers into the same range — the silent loss 
#6920 describes.
   
   Keep the hop, restore the ordering: give the message a `Promise[Unit]` the 
handler completes after the removals, and return it instead of `Future.Unit`.



##########
amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionExecutionManagerTestSupport.scala:
##########
@@ -164,6 +165,18 @@ object RegionExecutionManagerTestSupport {
     override def initState(): Unit = ()
 
     override def loadFromCheckpoint(chkpt: CheckpointState): Unit = ()
+
+    override def receive: Receive = {
+      case Coordinator.CleanupWorkerChannels(workerIds) =>

Review Comment:
   This duplicates `Coordinator.scala:193-198` line for line. So the spec's 
"Seed stale control channels to verify that successful termination removes 
them" fixture checks the harness's copy, not the production handler — drop 
`outputGateway.removeControlChannel` from `Coordinator` and the suite still 
passes.
   
   Lift the three removals into one helper on the `Coordinator` companion and 
call it from both sites. The assertion then exercises the real code again.
   
   The `case msg => super.receive(msg)` below has the same total-function 
problem as the production site.



##########
amber/src/main/scala/org/apache/texera/amber/engine/architecture/coordinator/Coordinator.scala:
##########
@@ -190,7 +190,15 @@ class Coordinator(
   }
 
   override def receive: Receive = {
-    super.receive orElse handleDirectInvocation orElse handleReplayMessages
+    case Coordinator.CleanupWorkerChannels(workerIds) =>
+      workerIds.foreach { workerId =>
+        cp.asyncRPCClient.inputGateway.removeControlChannel(workerId)
+        cp.asyncRPCClient.outputGateway.removeControlChannel(workerId)
+        cp.actorRefService.removeActorRef(workerId)
+      }
+
+    case msg =>
+      (super.receive orElse handleDirectInvocation orElse 
handleReplayMessages)(msg)
   }

Review Comment:
   The catch-all makes `receive` total, so `isDefinedAt` is always true and 
Pekko never calls `unhandled`. An unmatched message reaches this branch, fails 
the inner match, and throws `MatchError` inside the actor.
   
   That is reachable today: `ClusterListener` → `AmberClient.notifyNodeFailure` 
→ `ClientActor.scala:155` forwards a `WorkflowRecoveryMessage` to the 
Coordinator, which none of the three composed functions handle. `ClientActor` 
sets no `supervisorStrategy`, so the default restarts the Coordinator — wiping 
`cp` and re-running `initState()` mid-execution.
   
   `WorkflowWorker.receive:137` keeps the `orElse` shape for exactly this 
reason.
   
   ```suggestion
     private def handleCleanupWorkerChannels: Receive = {
       case Coordinator.CleanupWorkerChannels(workerIds) =>
         workerIds.foreach { workerId =>
           cp.asyncRPCClient.inputGateway.removeControlChannel(workerId)
           cp.asyncRPCClient.outputGateway.removeControlChannel(workerId)
           cp.actorRefService.removeActorRef(workerId)
         }
     }
   
     override def receive: Receive =
       handleCleanupWorkerChannels orElse
         super.receive orElse
         handleDirectInvocation orElse
         handleReplayMessages
   ```



##########
amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionExecutionManagerSpec.scala:
##########
@@ -234,10 +254,11 @@ class RegionExecutionManagerSpec
     assert(fixture.rpcProbe.endWorkerCalls.size == fixture.workerIds.size * 2)
   }
 
-  it should "default to a bounded ~1.4s termination budget" in {
-    // 4 attempts from a 200 ms base, doubling: 200 + 400 + 800 ms = ~1.4 s of 
waiting, not the
-    // former 150 x 200 ms (~30 s). This is the documented contract for how 
long a stuck region
-    // blocks before failing loudly; pin it so changes are deliberate.
+  it should "default to a bounded ~25.4s termination budget" in {
+    // 4 attempts from a 200 ms base, doubling: 200 + 400 + 800 ms = ~1.4 s of 
backoff, plus
+    // a 6 s timeout per attempt. Worst-case teardown is now ~25.4 s. This is 
the documented
+    // contract for how long a stuck region blocks before failing loudly; pin 
it so changes
+    // are deliberate.
     assert(RegionExecutionManager.DefaultMaxTerminationAttempts == 4)
     assert(RegionExecutionManager.DefaultKillRetryBaseBackoffMs == 200L)

Review Comment:
   This test calls itself the documented contract, so the figure and the 
assertions both have to hold. Neither does. `.within(killTimeout)` wraps the 
EndWorker collect and the gracefulStop collect separately, so an attempt bounds 
two stages. And `DefaultTerminationTimeoutMs`, now the dominant term, is never 
asserted.
   
   ```suggestion
     it should "default to a bounded termination budget" in {
       // 4 attempts from a 200 ms base, doubling: 200 + 400 + 800 ms = ~1.4 s 
of backoff. Each attempt
       // then bounds two stages: a 6 s timeout on the EndWorker collect, and 
gracefulStop's own 5 s
       // deadline on the stop collect -- ~11 s per attempt, ~45 s overall. 
This is the documented
       // contract for how long a stuck region blocks before failing loudly; 
pin it so changes
       // are deliberate.
       assert(RegionExecutionManager.DefaultMaxTerminationAttempts == 4)
       assert(RegionExecutionManager.DefaultKillRetryBaseBackoffMs == 200L)
       assert(RegionExecutionManager.DefaultTerminationTimeoutMs == 6000L)
   ```



##########
amber/src/main/scala/org/apache/texera/amber/engine/architecture/coordinator/Coordinator.scala:
##########
@@ -66,7 +66,7 @@ final case class CoordinatorConfig(
 )
 
 object Coordinator {
-
+  case class CleanupWorkerChannels(workerIds: Seq[ActorVirtualIdentity])

Review Comment:
   The two rationale comments that sat on these removals in 
`RegionExecutionManager` were dropped. This is now the only place the logic 
lives, so nothing records why the channels have to go — the part a future 
reader is most likely to simplify away.
   
   ```suggestion
     // Removing a worker's actorRef stops other actors from reaching it. 
Restarted regions reuse
     // actorId, so the control channels must go too: otherwise the coordinator 
resumes the old
     // control-message sequence numbers and the new worker discards its 
commands as duplicates.
     case class CleanupWorkerChannels(workerIds: Seq[ActorVirtualIdentity])
   ```



##########
amber/src/main/scala/org/apache/texera/amber/engine/architecture/coordinator/Coordinator.scala:
##########
@@ -44,7 +44,7 @@ import org.apache.texera.amber.engine.common.ambermessage.{
 import org.apache.texera.amber.engine.common.virtualidentity.util.{CLIENT, 
COORDINATOR, SELF}
 import org.apache.texera.amber.engine.common.{CheckpointState, SerializedState}
 import org.apache.texera.web.SessionState
-
+import org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity

Review Comment:
   This landed on the blank line that separated the `org.apache` imports from 
the `scala` block. Same in `RegionExecutionManager.scala:64` and 
`RegionExecutionManagerTestSupport.scala:60`.
   
   ```suggestion
   import org.apache.texera.amber.core.virtualidentity.ActorVirtualIdentity
   
   ```



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to