suprnation / cats-actors

Cats Actors framework for building apps which are reactive. Cats actors uses a conceptual actor model as a higher level abstraction for concurrency.
Apache License 2.0
105 stars 9 forks source link

Inconsistent Termination Order: Parent Actor Terminates Before Child #34

Closed Mitrug closed 1 week ago

Mitrug commented 2 weeks ago

Issue Description

When a PoisonPill is sent to a parent actor, it is expected that the child actor will terminate first, followed by the parent. However, in certain cases, the example below demonstrates that this termination order is not consistently followed, resulting in the parent sometimes terminating before the child. This issue appears intermittently, suggesting a possible race condition or timing inconsistency in the shutdown sequence.

Reproduction Example

The following test case aims to verify that the child actor stops before the parent when a PoisonPill is sent to the parent. However, the test periodically fails, with the child actor terminating after the parent.

it should "stop child first before parent" in {
  ActorSystem[IO]()
    .use(system =>
      for {
        lastToDieRef <- Ref.of[IO, String]("")

        child = new Actor[IO, Any] {
          override def postStop: IO[Unit] = IO.println("Stopping Child") *> lastToDieRef.set("Child") *> super.postStop
        }
        parent = new Actor[IO, Any] {
          override def preStart: IO[Unit] = super.preStart *> context.actorOf(child).void
          override def postStop: IO[Unit] = IO.println("Stopping Parent") *> lastToDieRef.set("Parent") *> super.postStop
        }

        parent <- system.actorOf(parent, "parent")

        _ <- parent ! PoisonPill
        _ <- system.waitForIdle()

        lastToDie <- lastToDieRef.get
      } yield lastToDie should be("Parent")
    )
    .unsafeToFuture()
}

The output of the above test case when the Parent dies first:

Stopping Parent
Stopping Child
[EventBus] => Debug([Path: kukku://06ae51ad-98b2-4767-a9dc-e54366101afb@localhost/dead-letter] [Name:dead-letter],class com.suprnation.actor.ReplyingActor,DeadLetter(DeathWatchNotification([System: 06ae51ad-98b2-4767-a9dc-e54366101afb] [Path: kukku://06ae51ad-98b2-4767-a9dc-e54366101afb@localhost/user/parent/2ca462f5-859d-4a19-b176-59fae88fd817] [name: 2ca462f5-859d-4a19-b176-59fae88fd817]},true,false),Some([System: 06ae51ad-98b2-4767-a9dc-e54366101afb] [Path: kukku://06ae51ad-98b2-4767-a9dc-e54366101afb@localhost/user/parent/2ca462f5-859d-4a19-b176-59fae88fd817] [name: 2ca462f5-859d-4a19-b176-59fae88fd817]}),Receiver([System: 06ae51ad-98b2-4767-a9dc-e54366101afb] [Path: kukku://06ae51ad-98b2-4767-a9dc-e54366101afb@localhost/user/parent] [name: parent]})))

The DeathWatchNotification originating from the Child Actor would be transferred to the DeadLetter Mailbox since the parent mailbox would already be closed.

Mitrug commented 1 week ago

I discussed this behaviour with @cloudmark and concluded that, while it is true we are not waiting for the child actors to terminate before swapping the parent's mailbox, the end result remains the same. In both cases, the parent actor still cannot process any new messages produced by the child, and those messages ultimately end up in the dead letter queue.