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
104 stars 9 forks source link

FSM - Ask (?) on an FSM actor doesn't work #13

Closed Mitrug closed 2 months ago

Mitrug commented 3 months ago

Description

Calling 'ask' (?) on a ReplyingActor created by an FSM returns the following error. In my case the FSM returns a List[Int]:

class scala.runtime.BoxedUnit cannot be cast to class scala.collection.immutable.List (scala.runtime.BoxedUnit and scala.collection.immutable.List are in unnamed module of loader 'app')
java.lang.ClassCastException: class scala.runtime.BoxedUnit cannot be cast to class scala.collection.immutable.List (scala.runtime.BoxedUnit and scala.collection.immutable.List are in unnamed module of loader 'app')
    at map @ com.suprnation.actor.InternalActorRef.$anonfun$$qmark$times$3(ReplyingActorRef.scala:229)
    at complete @ com.suprnation.actor.dispatch.mailbox.Mailboxes$$anon$2.$anonfun$systemEnqueue$6(Mailboxes.scala:167)
    at void @ com.suprnation.actor.engine.ActorCell$$anon$1.start(ActorCell.scala:199)
    at delay @ com.suprnation.actor.dispatch.mailbox.Mailboxes$$anon$2.enqueue(Mailboxes.scala:177)
    at recoverWith$extension @ com.suprnation.actor.dungeon.Creation.$anonfun$create$4(Creation.scala:81)
    at flatMap @ com.suprnation.actor.InternalActorRef.$anonfun$$qmark$times$2(ReplyingActorRef.scala:228)
    at apply @ com.suprnation.actor.ActorSystem$.$anonfun$apply$7(ActorSystem.scala:73)
    at flatMap @ com.suprnation.actor.InternalActorRef.$anonfun$$qmark$times$1(ReplyingActorRef.scala:227)
    at map @ com.suprnation.actor.InternalActorRef.$anonfun$assertCellActiveAndDo$1(ReplyingActorRef.scala:254)

Steps to Reproduce

sealed trait FsmParentState
case object FsmIdle extends FsmParentState
case object FsmRunning extends FsmParentState

sealed trait FsmRequest
case object FsmRun extends FsmRequest
case object FsmStop extends FsmRequest

it should "ask the FSM" in {
    (for {
      actorSystem <- ActorSystem[IO]("FSM Actor", (_: Any) => IO.unit).allocated.map(_._1)

      actor <- actorSystem.replyingActorOf(
        FSM[IO, FsmParentState, Int, FsmRequest, List[Int]]
          .when(FsmIdle)(sM => { case Event(FsmRun, _) =>
            sM.stayAndReply(List(1))
          })
          .withConfig(FSMConfig.withConsoleInformation)
          .startWith(FsmIdle, 0)
          .initialize
      )

      list <- actor ? FsmRun

      _ <- actorSystem.waitForIdle()
    } yield list).unsafeToFuture().map { messages =>
      messages should be(List(1))
    }
  }
PetrosPapapa commented 3 months ago

The problem is that actors reply differently in cats-actors than they do in Akka.

In Akka, an actor replies by sending the response via the mailbox. The ask pattern creates a temporary actor that receives the response and fulfills the promise of the ask.

This is how the FSM is implemented as well, so that stayAndReply in this example will send the reply/replies as a message. The receive method calls processMsg which returns F[Unit] so it always responds with Unit (which cannot be casted to List here). As things stand, you cannot ask an FSM actor.

In contrast to Akka, the ReplyingActor in cats-actors replies by returning the response in its ReplyingReceive method. This response then completes a Deferred, passing on the response directly and not through the mailbox.

In order to fix this, the FSM must ~create a ReplyingActor and~ issue the response via the ReplyingReceive method and not send it as a message.

jducoeur commented 3 months ago

In contrast to Akka, the ReplyingActor in cats-actors replies by returning the response in its ReplyingReceive method. This response then completes a Deferred, passing on the response directly and not through the mailbox.

Interesting -- I hadn't noticed this nuance before. So is the implication that ask suspends the requesting Actor's current effect (and blocks its mailbox) until the response comes back?

cloudmark commented 3 months ago

Yes. Although you can start and get a fiber back and processing continues. You could join afterwards with the fiber when you actually need a reply.