He-Pin opened a new issue, #3163:
URL: https://github.com/apache/pekko/issues/3163

   ### Summary
   
   Three Pekko Streams operators — `Sink.lazySink`, `Source.futureSource`, and 
`Sink.last` / `Sink.lastOption` / `Sink.takeLast` — can leave their 
materialized `Future` uncompleted forever when the stream is terminated 
abruptly (actor-system shutdown, materializer shutdown, async boundary 
isolation) before the operator reaches its normal completion path.
   
   Other operators with the same materialized-value shape (`Source.lazySource`, 
`Flow.lazyFlow`, `Sink.ignore`, `Sink.head` / `Sink.headOption`, `Sink.seq`, 
`Sink.eagerFutureSink`, etc.) all complete their `Promise` inside a `postStop` 
override using `AbruptStageTerminationException`. The three operators above are 
missing that override, leaving the future hanging.
   
   ### Symptom
   
   ```scala
   val mat: Future[M] = source.toMat(Sink.lazySink(factory))(Keep.right).run()
   // Actor system / materializer terminated before first element arrives
   Await.result(mat, 5.seconds)   // hangs forever, never fails
   ```
   
   Same shape with `Source.futureSource(longRunningFuture)` and `Sink.last` / 
`Sink.lastOption` / `Sink.takeLast`.
   
   ### Locations
   
   All three stages lack a `postStop` override that completes the materialized 
promise when the stage is finalized without having run through its normal 
completion path.
   
   1. **`Sink.lazySink`** — 
`stream/src/main/scala/org/apache/pekko/stream/impl/Sinks.scala`
      - Materialized `Promise[M]` is completed in `onPush`, `onUpstreamFinish`, 
`onUpstreamFailure`.
      - No `postStop` override.
      - Neighbouring `EagerFutureSink` in the same file has a correct 
`postStop` override that fails the promise with 
`AbruptStageTerminationException` — `LazySink` does not.
   
   2. **`Source.futureSource` / `FutureFlattenSource`** — 
`stream/src/main/scala/org/apache/pekko/stream/impl/fusing/GraphStages.scala`
      - Materialized `Promise[M]` is completed in `onFutureSourceCompleted` and 
in the initial handler's `onDownstreamFinish` (with `StreamDetachedException`).
      - No `postStop` override.
      - Source stages are the most vulnerable: with an async boundary, the 
source interpreter may have no boundary inputs to receive the abort error at 
all, so the completion paths may never fire.
      - Neighbouring stages (`LazySource`, `FutureFlow`, `MaybeSource`, 
`InputStreamSource`) all have `postStop` overrides.
   
   3. **`Sink.last` / `Sink.lastOption` / `Sink.takeLast` (shared 
`TakeLastStage`)** — 
`stream/src/main/scala/org/apache/pekko/stream/impl/Sinks.scala`
      - Materialized `Promise[immutable.Seq[T]]` is completed in 
`onUpstreamFinish` / `onUpstreamFailure`.
      - No `postStop` override.
      - Neighbouring `HeadOptionStage` (`Sink.head` / `Sink.headOption`) and 
`SeqStage` (`Sink.seq`) in the same file have a correct `postStop` override — 
`TakeLastStage` does not.
   
   ### Why the existing tests miss it
   
   The existing abrupt-termination tests use simple topologies with boundary 
sources (e.g. `Source.fromPublisher(probe)`). In those topologies, the abort 
error reliably propagates to the stage via `onUpstreamFailure`, which completes 
the promise through the normal failure path. The tests therefore exercise the 
**error-propagation** path, not the `postStop` fallback.
   
   When the error propagation path fails to reach the stage — async boundaries, 
complex topologies, or the source interpreter having no boundary inputs — 
`postStop` is the only remaining opportunity to complete the materialized 
promise. The interpreter always calls `postStop` during stage finalization (via 
`ActorGraphInterpreter.postStop` → `tryAbort` → `GraphInterpreter.finish` → 
`finalizeStage`). Without a `postStop` override, that opportunity is wasted and 
the promise hangs.
   
   ### Reproduction sketch
   
   ```scala
   // lazySink variant
   val mat = Source.empty
     .delay(10.seconds)
     .toMat(Sink.lazySink(_ => Sink.ignore))(Keep.right)
     .run()
   // Terminate the materializer before the first element.
   // mat should fail with AbruptStageTerminationException but instead hangs 
forever.
   ```
   
   Similar sketches apply for `Source.futureSource` (pass an uncompleted 
future, then terminate) and `Sink.last` (terminate before upstream finishes).
   
   ### Proposed fix
   
   For each of the three stages, add a `postStop` override completing the 
materialized promise when not already completed, matching the pattern used in 
neighboring operators:
   
   ```scala
   override def postStop(): Unit = {
     if (!promise.isCompleted)
       promise.failure(new AbruptStageTerminationException(this))
   }
   ```
   
   Apply this to:
   - `LazySink`'s `stageLogic` in `Sinks.scala`
   - `FutureFlattenSource`'s `GraphStageLogic` in `GraphStages.scala`
   - `TakeLastStage` in `Sinks.scala`
   
   Directional tests should cover abrupt termination with an async boundary 
between the stage under test and its upstream/downstream, so that the 
error-propagation path does NOT reach the stage and `postStop` is the only 
fallback. Existing abrupt-termination tests in `EagerFutureSinkSpec`, 
`HeadSinkSpec`, and `TakeLastSinkSpec` provide templates; new tests should 
assert the `AbruptStageTerminationException` via the `postStop` fallback.
   
   ### Environment
   
   - Apache Pekko streams, current `main`
   - Affects: `Sink.lazySink`, `Sink.lazySinkAsync` variants, 
`Source.futureSource`, `Sink.last`, `Sink.lastOption`, `Sink.takeLast`


-- 
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]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to