ifta-zoy opened a new issue, #3345:
URL: https://github.com/apache/pekko/issues/3345

   # Summary
   
   When materializing a BroadcastHub on an actor's materializer, 
stopping/crashing the actor does not complete the BroadcastHub Source.
   
   # Analysis
   
   Two independent actors are running in this situation: 
   The first ("Worker") is materializing a Source via a `BroadcastHub.sink`, 
running with a Materializer bound to the actor. The source created by the 
BroadcastHub is then passed to a different actor ("Listener") that runs it to 
itself using an ActorSink.
   
   When the Worker stops (or is crashed), the source created by the 
BroadcastHub is not terminated (i.e. the Listener does not receive a 
Completed/Error message). 
   The source that runs into the BroadcastHub is cancelled, as expected. 
   
   If the source that runs the BroadcastHub is completed/failed manually, the 
BroadcastHub source completes as expected.
   
   The reproducer has a third actor that sends a Crash/Stop message to the 
Worker after a couple of seconds.
   
   # Reproducer
   
   ```Scala
   import org.apache.pekko.NotUsed
   import org.apache.pekko.actor.typed.scaladsl.Behaviors
   import org.apache.pekko.actor.typed.{ActorRef, ActorSystem, Behavior}
   import org.apache.pekko.dispatch.ExecutionContexts
   import org.apache.pekko.stream.scaladsl.{BroadcastHub, Keep, Source, 
SourceQueueWithComplete}
   import org.apache.pekko.stream.typed.scaladsl.ActorSink
   import org.apache.pekko.stream.{Materializer, OverflowStrategy}
   
   import scala.concurrent.duration.DurationInt
   import scala.util.{Failure, Success}
   
   // Actor 1: creates a Source and materializes it to a BroadcastHub. Uses 
Materializer bound to the actor.
   sealed trait WorkerMsg
   case object CrashOrStop    extends WorkerMsg
   object Worker {
     private def running(queue: SourceQueueWithComplete[Int]): 
Behavior[WorkerMsg] = Behaviors.receiveMessage {
       case CrashOrStop =>
   //      queue.complete()                                // => expected 
Behavior -- Source is closed
   //      queue.fail(RuntimeException("Worker crashed"))  // => expected 
Behavior -- same
   //      throw RuntimeException("Worker has crashed")    // => unexpected 
Behavior -- Source not closed
         Behaviors.stopped                                 // => unexpected 
Behavior -- same
     }
   
     def apply(listenerRef: ActorRef[ListenerMsg]): Behavior[WorkerMsg] = 
Behaviors.setup { ctx =>
       given Materializer = Materializer(ctx)
   
       // Primary Flow materializing to the BroadcastHub
       val ((queue, terminationFuture), source) = Source
         .queue[Int](10, OverflowStrategy.backpressure)
         .watchTermination() { (mat, future) => (mat, future) }
         .toMat(BroadcastHub.sink(bufferSize = 4))(Keep.both)
         .run()
   
       // (no influence on the issue -- just for seeing that this Flow really 
completes)
       terminationFuture.onComplete {
         case Success(_)  => println("Worker: BroadcastHub Flow completed 
successful")
         case Failure(ex) => println(s"Worker: BroadcastHub Flow completed with 
error: $ex")
       }(using ExecutionContexts.global())
   
       listenerRef ! SourceOpened(source)
   
       // Minified example -- no one writing to the queue. With a separate 
writing actor the issue still happens.
   
       running(queue)
     }
   }
   
   // Actor 2: Receives the source coming from Actor 1 (Worker)'s BroadcastHub 
and materializes it to itself using an ActorSink.
   // Uses materializer bound to actor
   sealed trait ListenerMsg
   case class  SourceOpened(source: Source[Int, NotUsed])  extends ListenerMsg
   case object SourceCompleted                             extends ListenerMsg
   case class  SourceFailed(ex: Throwable)                 extends ListenerMsg
   case class  PrintMsg(s: String)                         extends ListenerMsg
   object Listener {
     def apply(): Behavior[ListenerMsg] = Behaviors.receive {
       case (ctx, PrintMsg(s)) =>
         Behaviors.same // pseudo-processing
       case (ctx, SourceFailed(ex)) =>
         println(s"Listener: Source Closed -- $ex") // either this or 
SourceCompleted expected
         Behaviors.stopped
       case (ctx, SourceCompleted) =>
         println("Listener: Source Closed successfully")
         Behaviors.stopped
       case (ctx, SourceOpened(incomingSource)) =>
         given Materializer = Materializer(ctx)
   
         // Second actor materializing the BroadcastHub's output source
         incomingSource
           .map(i => PrintMsg(i.toString))
           .to(ActorSink.actorRef(ctx.self, SourceCompleted, ex => 
SourceFailed(ex)))
           .run()
   
         Behaviors.same
     }
   }
   
   // Actor 3: sends the Stop/Crash message to the Worker after a while
   sealed trait WorkerTerminatorMsg
   case object TerminateWorker extends WorkerTerminatorMsg
   object WorkerTerminator {
     def apply(worker: ActorRef[WorkerMsg]): Behavior[WorkerTerminatorMsg] = 
Behaviors.withTimers[WorkerTerminatorMsg] { timers =>
       timers.startSingleTimer(TerminateWorker, 3.second)
       Behaviors.receiveMessage {
         case TerminateWorker =>
           println("Time to terminate the worker")
           worker ! CrashOrStop
           Behaviors.stopped
       }
     }
   }
   
   object Guardian {
     def apply(): Behavior[NotUsed] = Behaviors.setup { context =>
       val listener = context.spawn(Listener(), "Listener")
       val workerRef = context.spawn(Worker(listener), "Worker")
   
       context.spawn(WorkerTerminator(workerRef), "Worker-Terminator")
   
       Behaviors.empty
     }
   }
   
   @main
   def main(): Unit = {
     ActorSystem(Guardian(), "MySystem")
   }
   ```


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