Akkatype with classic actors giving me an error: Unsupported access to ActorContext from outside of Actor
I am getting the following runtime error in my akka application (using both akka typed and classic)
java.lang.UnsupportedOperationException: Unsupported access to ActorContext from the outside of Actor[akka://my-classic-actor-system/user/ChatServer#1583147696]. No message is currently processed by the actor, but ActorContext was called from Thread[my-classic-actor-system-akka.actor.default-dispatcher-5,5,run-main-group-0].
def main(args: Array[String]): Unit = {
val logger = LoggerFactory.getLogger(getClass)
logger.debug("Server starting yo...")
implicit val system = akka.actor.ActorSystem("my-classic-actor-system")
implicit val typedGuardian: ActorRef[Any] =
system.spawn(HttpWebsocketServer(), "ChatServer")
val client = system.actorOf(Client.props(remote), "clientActor")
//..
}
The source of the error is inside of HttpWebsocketServer which is taken from ( https://github.com/johanandren/chat-with-akka-http-websockets/blob/akka-2.6/src/main/scala/chat/Server.scala#L101):
object HttpWebsocketServer {
def apply(): Behavior[Any] = {
Behaviors.setup[Any] { context =>
implicit val ec: ExecutionContext = context.executionContext
val system = context.system
val chatRoom = context.spawn(ChatRoom(), "chat")
val userParent = context.spawn(SpawnProtocol(), "users")
val maker =
context.spawn(websock.Maker(), "mmaker")
val route =
path("chat") {
get {
handleWebSocketMessages(
newUserFlow(system, chatRoom, userParent)
)
}
}
// needed until Akka HTTP has a 2.6 only release
implicit val materializer: Materializer =
SystemMaterializer(context.system).materializer
implicit val classicSystem: akka.actor.ActorSystem =
context.system.toClassic
Http()
.bindAndHandle(route, "0.0.0.0", 9000)
// future callback, be careful not to touch actor state from in here
.onComplete {
case Success(binding) =>
context.log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
context.log.error("Server failed to start, terminating")
context.system.terminate()
}
Behaviors.empty
}
}
}
build.sbt
val AkkaVersion = "2.6.18"
val AkkaHttpVersion = "10.2.7"
libraryDependencies ++= Seq(
"io.netty" % "netty-all" % "4.1.68.Final",
"com.typesafe.akka" %% "akka-actor" % AkkaVersion,
"com.typesafe.akka" %% "akka-actor-typed" % AkkaVersion,
"com.typesafe.akka" %% "akka-stream" % AkkaVersion,
"com.typesafe.akka" %% "akka-stream-typed" % AkkaVersion,
"com.typesafe.akka" %% "akka-http" % AkkaHttpVersion,
"com.typesafe.akka" %% "akka-http-spray-json" % AkkaHttpVersion,
"ch.qos.logback" % "logback-classic" % "1.2.10",
scalaTest % Test
)
If I comment out below in my code I don't get the runtime error, so it looks like that is the source of the issue:
implicit val materializer: Materializer =
SystemMaterializer(context.system).materializer
implicit val classicSystem: akka.actor.ActorSystem =
context.system.toClassic
Http()
.bindAndHandle(route, "0.0.0.0", 9000)
// future callback, be careful not to touch actor state from in here
.onComplete {
case Success(binding) =>
context.log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
context.log.error("Server failed to start, terminating")
context.system.terminate()
}
I'm not sure what I can do to solve this issue, I am creating actors from either the system or child actors.
Solution 1:
You are not allowed to use context
outside of an actor. And you do it in callback of your future.
case Success(binding) =>
context.log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
context.log.error("Server failed to start, terminating")
context.system.terminate()
The problem is that actor context is treated as internal state of an actor and must not be accessed outside of the scope. Even your comment says that
// future callback, be careful not to touch actor state from in here
The solution is to pre-reference your log
and system
(you have this already) in a val
outside of the callback. The callback can look like
case Success(binding) =>
log.debug(
s"Started server at ${binding.localAddress.getHostString}:${binding.localAddress.getPort}"
)
case Failure(ex) =>
ex.printStackTrace()
log.error("Server failed to start, terminating")
system.terminate()