Yicong-Huang commented on code in PR #7122:
URL: https://github.com/apache/texera/pull/7122#discussion_r3773356916
##########
amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionExecutionManager.scala:
##########
@@ -195,41 +205,41 @@ class RegionExecutionManager(
}
}.toSeq
- val endWorkerFuture: Future[Unit] =
- Future.collect(endWorkerRequests).unit
-
- // 2. Send GracefulStops only after 1 has finished
- 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
+ val terminationAttempt =
+ Future
+ .collect(endWorkerRequests)
+ .unit
+ .flatMap { _ =>
+ // 2. Only send GracefulStops after all EndWorkers have succeeded.
+ val gracefulStopRequests =
+ regionExecution.getAllOperatorExecutions.flatMap {
+ case (_, opExec) =>
+ opExec.getWorkerIds.map { workerId =>
+ val actorRef = actorRefService.getActorRef(workerId)
+ gracefulStop(actorRef, ScalaDuration(5,
TimeUnit.SECONDS)).asTwitter()
+ }
+ }.toSeq
- Future.collect(gracefulStops).unit
- }
+ Future.collect(gracefulStopRequests).unit
+ }
+ .within(killTimeout)
Review Comment:
Round 2 bounded each stage separately; this collapses both into one window
(`grep -c "within(killTimeout)"` returns 2 at `d6a9695`, 1 here). The 6 s is
now a shared budget. Whatever the EndWorker round-trip spends comes off
`gracefulStop`, whose own deadline is 5 s (`:219`) — reachable only if
EndWorker answers within 1 s.
So a worker taking 4-5 s to stop, the slow-PVM case #6920 gives as its
repro, now needs its EndWorker round-trip under 1-2 s. The retry cannot recover
it: attempt 1 already sent the stop, so attempt 2's EndWorker reaches a
stopping worker and never answers. On `main` that teardown just completed.
Derive the outer window from the inner deadline instead of tuning a literal.
Hoist gracefulStop's 5 s into a constant and set `DefaultTerminationTimeoutMs`
to it plus a stated drain allowance, so the relationship cannot silently invert
again.
##########
amber/src/main/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionExecutionManager.scala:
##########
@@ -195,41 +205,41 @@ class RegionExecutionManager(
}
}.toSeq
- val endWorkerFuture: Future[Unit] =
- Future.collect(endWorkerRequests).unit
-
- // 2. Send GracefulStops only after 1 has finished
- 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
+ val terminationAttempt =
+ Future
+ .collect(endWorkerRequests)
+ .unit
+ .flatMap { _ =>
+ // 2. Only send GracefulStops after all EndWorkers have succeeded.
+ val gracefulStopRequests =
+ regionExecution.getAllOperatorExecutions.flatMap {
+ case (_, opExec) =>
+ opExec.getWorkerIds.map { workerId =>
+ val actorRef = actorRefService.getActorRef(workerId)
+ gracefulStop(actorRef, ScalaDuration(5,
TimeUnit.SECONDS)).asTwitter()
+ }
+ }.toSeq
- Future.collect(gracefulStops).unit
- }
+ Future.collect(gracefulStopRequests).unit
+ }
+ .within(killTimeout)
- // 3. Log whether the kills were successful
- gracefulStopRequests.transform {
+ // 3. Cleanup only after graceful termination succeeds.
Review Comment:
`Cleanup` is the noun; the sibling step comments are imperative.
```suggestion
// 3. Clean up only after graceful termination succeeds.
```
##########
amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionExecutionManagerSpec.scala:
##########
@@ -141,6 +141,45 @@ class RegionExecutionManagerSpec
assert(workerState(fixture) == WorkerState.TERMINATED)
}
+ it should "retry EndWorker when the termination timeout expires" in {
+ val fixture = createSingleRegionFixture(
+ endWorkerResponse = _ => None,
+ maxTerminationAttempts = 2,
+ killRetryBaseBackoffMs = fastRetryBackoffMs,
+ terminationTimeoutMs = 10L
+ )
+
+ launchRegion(fixture.manager)
+ val completion = requestRegionCompletion(fixture.manager)
+
+ val failure = intercept[IllegalStateException] {
+ await(completion)
+ }
+
+ assert(failure.getMessage.contains("could not be terminated after 2
attempts"))
+ assert(fixture.rpcProbe.endWorkerCalls.size == 2)
+ assert(!fixture.manager.isCompleted)
+ }
+
+ it should "clean up control channels and actor refs after successful
termination" in {
Review Comment:
These three assertions are a strict subset of `"send gracefulStop only after
EndWorker succeeds"` (`:101-104`), which already checks all of them plus the
worker state.
Despite the name it also does not reach `Coordinator.receive`:
`CoordinatorHarness` carries its own `handleCleanupWorkerChannels`
(`RegionExecutionManagerTestSupport.scala:171-179`), so deleting the case from
`Coordinator.receive` still leaves the suite green.
I would drop the test — it buys no coverage over `:101-104`.
##########
amber/src/test/scala/org/apache/texera/amber/engine/architecture/scheduling/RegionExecutionManagerSpec.scala:
##########
@@ -251,7 +293,8 @@ class RegionExecutionManagerSpec
Time.withCurrentTimeFrozen { _ =>
val fixture = createSingleRegionFixture(
endWorkerResponse = _ => Some(transientEndWorkerFailure),
- killRetryTimer = timer
+ killRetryTimer = timer,
+ terminationTimeoutMs = Long.MaxValue
Review Comment:
Worth naming what `Long.MaxValue` means here. It reads as an arbitrary large
number, but it is a sentinel: `Duration.fromMilliseconds(Long.MaxValue)`
saturates to `Duration.Top`, and `within(Top)` short-circuits without
scheduling.
That matters, because `RecordingInlineTimer.scheduleOnce` runs whatever it
is handed *inline*. A timeout task actually scheduled here would fire
immediately and fail both frozen-time tests confusingly. A named constant or a
one-line comment would keep the next reader from rediscovering it. (Mine to
have caught last round.)
--
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]