Below I have a minimal repro of an issue I’m having with the messageAdapter
pattern in Akka Typed. When you run it, it will print out the log messages below, which breaks the defined Message
types.
As far as I can tell, messageAdapter
creates new adapters when you call it that’s conceptually something like a Map[Class[U], U => T]
so you can look up the conversion function when a certain message type comes in, but to get the Class[U]
it uses a ClassTag
, which is a runtime reflection mechanism that suffers from type erasure.
So you have a Seq -> (Seq => Msg2)
overriding the previously registered adapter Seq -> (Seq => Msg1)
, instead of Seq[Int] => (Seq[Int] => Msg1)
etc. Scala does have TypeTag
, which does not suffer from type erasure (slightly different API, but conceptually similar), but it’s a Scala compiler specific thing of which Java has no equivalent. I’m guessing this is a known problem whose source is Java interop.
Are there any good workarounds here? I know you can do something like Behaviors.receive[Seq[Int] => Unit]
and actorA ! (msg => context.self ! Msg1(msg))
which solves the problem, but feels hacky. I’ve also thought about writing something along the lines of my own MessageAdapter
class, but a naive implementation needs to carry both the source and destination types, which is the problem the messageAdapter
existed to solve in the first place.
[2023-06-14 16:26:54,814] [INFO] [Main$] [] [Guardian-akka.actor.default-dispatcher-3] - Msg2 Some(1) MDC: {akkaAddress=akka://Guardian, akkaSource=akka://Guardian/user, sourceActorSystem=Guardian}
[2023-06-14 16:26:54,816] [INFO] [Main$] [] [Guardian-akka.actor.default-dispatcher-3] - Msg2 Some(a) MDC: {akkaAddress=akka://Guardian, akkaSource=akka://Guardian/user, sourceActorSystem=Guardian}
object Main extends App {
sealed trait Message
object Message {
case class Msg1(seq: Option[Int]) extends Message
case class Msg2(seq: Option[String]) extends Message
}
val guardian = Behaviors.setup[Message] { context =>
val actorA = context.spawn(
Behaviors.receive[ActorRef[Option[Int]]] { case (_, replyTo) =>
replyTo ! Some(1)
Behaviors.stopped
},
"ActorA"
)
val actorB = context.spawn(
Behaviors.receive[ActorRef[Option[String]]] { case (_, replyTo) =>
replyTo ! Some("a")
Behaviors.stopped
},
"ActorB"
)
actorA ! context.messageAdapter(Msg1)
actorB ! context.messageAdapter(Msg2)
Behaviors.receive[Message] { case (context, msg) =>
msg match {
case Msg1(opt: Option[Int]) =>
context.log.info(s"Msg1 $opt")
case Msg2(opt: Option[String]) =>
context.log.info(s"Msg2 $opt")
}
Behaviors.same
}
}
val actorSystem = ActorSystem(guardian, "Guardian")
}