How can we Serialize/Deserialize ActorRef ?
Can we use “ActorRefResolver”
final ActorRef<T> myActor = ...
final String str = ActorRefResolver.get(system).toSerializationFormat(myActor);
How can we Serialize/Deserialize ActorRef ?
Can we use “ActorRefResolver”
final ActorRef<T> myActor = ...
final String str = ActorRefResolver.get(system).toSerializationFormat(myActor);
That is correct. ActorRefResolver
provides toSerializationFormat
for serialization to String and resolveActorRef
for deserialization from String.
While serializing typed cluster remote ActorRef via ActorRefResolver
there seems to be a potential problem.
Please refer the below example to serialize typed cluster remote ActorRef:
implicit val timeout: Timeout = Timeout(5.seconds)
private val system1 = ActorSystem(Behaviors.empty, "system-1")
private val system2 = ActorSystem(Behaviors.empty, "system-2")
private val actorRefOne: ActorRef[Nothing] = Await.result(system1.systemActorOf(Behaviors.empty, "actor-1"), 5.seconds)
ActorRefResolver(system2).toSerializationFormat(actorRefOne)
Here, ActorRefResolver
requires an ActorSystem and uses its default address for serializing actorRefOne
. Hence, the user can make a mistake of providing some random ActorSystem (system2
) other than the one to which actorRefOne
belongs to i.e. system1
. This problem remains unknown to the user until some runtime failure occurs.
This was not a problem in untyped api of serialization where default address was fetched from ActorRef directly
Serialization.serializedActorPath(actorRefOne.toUntyped)
So, shouldn’t we have the same safety like untyped api in ActorRefResolver
as well ?
I think you are right that we can improve this. Please create an issue.
Thanks @patriknw. Will create an issue and follow up there.
After the fix https://github.com/akka/akka/pull/27391/ we see that ActorRefResolver(system)
can serialize ActorRef
only if it belongs to that system
as per the following implementation:
override def toSerializationFormat[T](ref: ActorRef[T]): String = {
def toSerializationFormatWithAddress =
ref.path.toSerializationFormatWithAddress(system.address)
ref.toClassic match {
case a: ActorRefWithCell =>
val originSystem = a.underlying.system.asInstanceOf[ExtendedActorSystem]
if (originSystem eq classicSystem)
toSerializationFormatWithAddress
else
throw new IllegalArgumentException(
s"ActorRefResolver for ActorSystem [${classicSystem.provider.getDefaultAddress}] shouldn't be used for " +
"serialization of ActorRef that originates from another ActorSystem " +
s"[${originSystem.provider.getDefaultAddress}]. Use the ActorRefResolver for that system instead.")
case _ =>
// no origin system information for RemoteActorRef or MinimalActorRef, so just use the
// one for this extension. That is correct for RemoteActorRef, but MinimalActorRef
// could be wrong. However, since we don't allow usage of "wrong" ActorSystem for
// ordinary ActorRef the users will learn not to make that mistake.
toSerializationFormatWithAddress
}
}
This works well in a clustered architecture when node1 creates an ActorRef
, serialzes it and shares it to node2 in the cluster as replyTo
. But the problem arrises when node2 deserializes the actor ref coming from node1 and later decides to pass on to node3 so that node3 can directly talk to node1. In the later scenario, node2 fails to serialize actor ref from node1 as per the current implementation.
Hence, can we cater to the scenario where node2 can serialize actor ref from node1 and send it to node3 and change the implementation as following:
override def toSerializationFormat[T](ref: ActorRef[T]): String = {
def toSerializationFormatWithAddress =
ref.path.toSerializationFormatWithAddress(system.address)
ref.toClassic match {
case a: ActorRefWithCell =>
val originSystem = a.underlying.system.asInstanceOf[ExtendedActorSystem]
ref.path.toSerializationFormatWithAddress(originSystem.provider.getDefaultAddress)
case _ =>
// no origin system information for RemoteActorRef or MinimalActorRef, so just use the
// one for this extension. That is correct for RemoteActorRef, but MinimalActorRef
// could be wrong. However, since we don't allow usage of "wrong" ActorSystem for
// ordinary ActorRef the users will learn not to make that mistake.
toSerializationFormatWithAddress
}
}
Are node2 and node3 two separate ActorSystems running in the same JVM, and you pass the ActorRef between them by reference without sending it in a message?
It’s wrong to pass an ActorRef by reference from one system to another system. Sometimes it just works anyway but sometimes not. Such refs should always be retrieved in the same way as if the other ActorSystem was remote (in other JVM), i.e. they will be serialized again.
The change of this in the PR was intentional.
Yes @patriknw you are right. We are having node2 and node3 as two different actor systems in the same JVM. We now understand the problem that would be caused by accessing ActorRef by reference and we will change our code base such that ActorRefs are accessed via messages(or serailization) and not via references.
Thanks for the explanation!
FYI: The ActorRefResolver requires an ActorSystem. That makes it exceedling difficult to use from a ScalaPB TypeMapper which must be provided to a ScalaPB generated companion class via a trait. I’ve tried a variety of options including thread local storage to provide that ActorSystem with no viable solutions. This forces us to violate type safety as we must define our ActorRef[_] values as string and manually do the conversion in application code. So, this precludes us doing things in protobuffers (akkaGrpc) like:
message WithAReplyTo {
string replyTo = 1 [(scalapb.field).type = "ActorRef[ExpectedType]";
}
That is the construct that ScalaPB provides to do automatic type mapping between the ActorRef and the string. Also, even this mechanism is cumbersome because you have to provide a TypeMapper for each “ExpectedType” and there could be many in a large system.
It should be possible to grab the ActorSystem from Serialization.getCurrentTransportInformation.system
. You find some more information in https://github.com/akka/akka/issues/27975
Thanks, Patrick. We went down that road but it doesn’t work in non-serialization contexts as often happens in our code. When there is no current transport information available (non-serialization thread), then an exception is thrown. This was considered a non-viable solution because we use those TypeMapper’d classes in non-serialization threads frequently. The thread you mentioned describes this pretty well.