ADR-0007 ZIO-fication of Responders
Date: 2023-02-20
Status
Accepted
Context
In order to remove all Akka dependencies, we have to migrate the existing Responders to a ZIO based
implementation.
This migration should be possible to do on a per Responder basis so that we do not do a single "big-bang" release with
too much code changed at once.
Status Quo
The central and only Actor is the RoutingActor which contains instances of each Responder as a field.
Each of the Responders needs an ActorRef to the RoutingActor and used the Akka "ask pattern" for communication
with the other Responders.
This means a Responder can only be created inside the RoutingActor because the RoutingActor must know
every Responder in order to route the message but the Responder needs the ActorRef in order to communicate with
the other Responders.
This leads to a circular dependency between the RoutingActor and all Akka based Responders.
Goal
In the long term all Responders do not contain any Akka dependency anymore and all implementations currently
returning a Future will return a zio.Task.
The zio.Task is a very suitable replacement for the Future because:
- a
Future[A]will complete with either a valueAor with a failureThrowable. - a
zio.Task[A]will succeed with either a valueAor fail with an error of typeThrowable.
Ideally all Responders will directly call the necessary components directly through invoking methods.
However, this will not be possible in the beginning as there are Responders who call on each other creating yet another
circular dependency which we cannot simply recreate with ZLayer dependency injection.
Hence, a message like communication pattern through a central component the MessageRelay will be introduced which
can replace the existing Akka "ask pattern" one to one in the ziofied component.
Solution
The MessageRelay is capable of relaying message to subscribed MessageHandlers and replaces the existing Akka "ask
pattern" with the RoutingActor.
Messages which will have a MessageHandler implementation must extend the RelayedMessage trait so that these are
routed to the MessageRelay from the RoutingActor.
All other messages will be handled as before.
In ziofied Responders we can use the MessageRelay for communication with all other Responders in a similar fashion
as the Akka "ask pattern" by invoking the method MessageRelay#ask(ResponderRequest): Task[Any].
A special MessageHandler will route all messages which do not implement the RelayedMessage trait back to
the RoutingActor, this is the AppRouterRelayingMessageHandler.
In the long run we will prefer to invoke methods on the respective ziofied services directly.
This is now already possible for example with the TriplestoreServive, i.e. instead of
calling MessageRelay#ask[SparqlSelectResul](SparqlSelectRequest) it is much easier and more importantly typesafe to
call TriplestoreService#sparqlHttpSelect(String): UIO[SparqlSelectResult].
Communication between Akka based Responder and another Akka based Responder
Nothing changes with regard to existing communication patterns:
sequenceDiagram
autonumber
AkkaResponder ->> RoutingActor: "ask(Request)"
activate RoutingActor
RoutingActor ->> OtherAkkaResponder: "sends message to"
activate OtherAkkaResponder
OtherAkkaResponder ->> RoutingActor: "returns response"
deactivate OtherAkkaResponder
RoutingActor ->> AkkaResponder: "returns response"
deactivate RoutingActor
Communication between Akka based Responder and ziofied Responder
The AkkaResponder code remains unchanged and will still ask the ActorRef to the RoutingActor.
The RoutingActor will forward the message to the MessageRelay and return its response to the AkkaResponder.
sequenceDiagram
autonumber
AkkaResponder ->> RoutingActor: "ask(RelayedMessage)"
activate RoutingActor
RoutingActor ->> MessageRelay: "messageRelay.ask(RelayedMessage)"
activate MessageRelay
MessageRelay ->> MessageRelay: "finds MessageHandler"
MessageRelay ->> ZioResponder: "calls .handle(Request)"
activate ZioResponder
ZioResponder ->> MessageRelay: "returns response"
deactivate ZioResponder
MessageRelay ->> RoutingActor: "returns response"
deactivate MessageRelay
RoutingActor ->> AkkaResponder: "returns response"
deactivate RoutingActor
Communication between ziofied Responder and Akka based Responder
The AppRouterRelayingMessageHandler route all messages which do not implement the RelayedMessage trait to
the RoutingActor.
sequenceDiagram
autonumber
ZioResponder ->> MessageRelay: "ask(Request)"
activate MessageRelay
MessageRelay ->> MessageRelay: "finds MessageHandler"
MessageRelay ->> AppRouterRelayingMessageHandler: "calls .handle(Request)"
activate AppRouterRelayingMessageHandler
AppRouterRelayingMessageHandler ->> RoutingActor: "sends message to"
deactivate AppRouterRelayingMessageHandler
activate RoutingActor
RoutingActor ->> AkkaResponder: "calls AkkaResponder"
activate AkkaResponder
AkkaResponder ->> RoutingActor: "returns response"
deactivate AkkaResponder
RoutingActor ->> MessageRelay: "returns response"
deactivate RoutingActor
MessageRelay ->> ZioResponder: "returns response"
deactivate MessageRelay
Communication between two ziofied Responders
Variant using the MessageRelay
sequenceDiagram
autonumber
ZioResponder ->> MessageRelay: "ask(Request)"
activate MessageRelay
MessageRelay ->> MessageRelay: "finds MessageHandler"
MessageRelay ->> OtherZioResponder: "calls .handle(Request)"
activate OtherZioResponder
OtherZioResponder ->> MessageRelay: "returns response"
deactivate OtherZioResponder
MessageRelay ->> ZioResponder: "returns response"
deactivate MessageRelay
Variant if other Responder is a direct dependency
sequenceDiagram
autonumber
ZioResponder ->> TriplestoreService: "calls method"
activate TriplestoreService
TriplestoreService ->> ZioResponder: "returns response"
deactivate TriplestoreService
Decision
In preparation of the move from Akka to ZIO,
it was decided that the Responders should be ported to use return ZIOs and the MessageRelay
instead of Futures and the ActorRef to the RoutingActor.
Consequences
In a first step only the Responders are going to be ported, one by one, to use the above pattern.
The Akka Actor System still remains, will be used in the test and will be removed in a later step.
Due to the added indirections and the blocking nature of Unsafe.unsafe(implicit u => r.unsafe.run(effect))
it is necessary to spin up more RoutingActor instances as otherwise deadlocks will occur.
This should not be a problem as any shared state, e.g. caches,
is not held within the RoutingActor or one of its contained Responder instances.