Il est possible de créer des sources et des puits à partir d'acteurs en utilisant respectivement les méthodes Source.actorPublisher()
et Sink.actorSubscriber()
. Mais est-il possible de créer un acteur Flow
?
Conceptuellement, il ne semble pas y avoir de bonne raison de ne pas le faire, car il implémente les deux traits ActorPublisher
et ActorSubscriber
, mais malheureusement, l'objet Flow
ne dispose d'aucune méthode pour le faire. Dans this excellent article de blog, cela est fait dans une version antérieure d’Akka Streams. La question est donc de savoir si cela est possible également dans la dernière version (2.4.9).
Je fais partie de l'équipe Akka et j'aimerais utiliser cette question pour clarifier quelques points concernant les interfaces brutes des flux réactifs. J'espère que vous trouverez cela utile.
Plus particulièrement, nous publierons bientôt plusieurs messages sur le blog de l'équipe Akka sur la création d'étapes personnalisées, y compris Flows, alors gardez un œil dessus.
N'utilisez pas ActorPublisher/ActorSubscriber
Veuillez ne pas utiliser ActorPublisher
et ActorSubscriber
. Ils sont trop bas et vous pourriez les implémenter de telle manière à violer la spécification des flux réactifs . Ils sont un vestige du passé et n'étaient alors que "mode utilisateur expérimenté". Il n'y a vraiment aucune raison d'utiliser ces classes de nos jours. Nous n'avons jamais fourni de moyen de créer un flux, car la complexité est simplement explosive si elle était exposée comme une API d'acteur "brute" que vous pouvez implémenter et obtenir toutes les règles correctement implémentées .
Si vous voulez vraiment implémenter des interfaces brutes ReactiveStreams, utilisez s'il vous plaît le spécification de TCK pour vérifier que votre implémentation est correcte. Vous serez probablement pris au dépourvu par certains des cas complexes les plus complexes comme Flow
(ou, dans la terminologie SR, que Processor
doit gérer).
Il est possible de construire la plupart des opérations sans passer à bas niveau
De nombreux flux que vous devriez pouvoir créer simplement en construisant à partir d'un Flow[T]
et en y ajoutant les opérations nécessaires, à titre d'exemple:
val newFlow: Flow[String, Int, NotUsed] = Flow[String].map(_.toInt)
Ce qui est une description réutilisable du flux.
Puisque vous parlez du mode utilisateur expérimenté, il s'agit de l'opérateur le plus puissant du DSL lui-même: statefulFlatMapConcat
. La grande majorité des opérations opérant sur des éléments en flux simple peut être exprimée à l'aide de celle-ci: Flow.statefulMapConcat[T](f: () ⇒ (Out) ⇒ Iterable[T]): Repr[T]
.
Si vous avez besoin de minuteries, vous pouvez Zip
avec un Source.timer
etc.
GraphStage est l'API la plus simple et la plus sûre pour créer des étapes personnalisées
Au lieu de cela, la construction de Sources/Flows/Sinks a sa propre API puissante et sûre: la GraphStage
. Veuillez lire la documentation sur la construction de GraphStages personnalisés (il peut s'agir d'un Sink/Source/Flow ou même de n'importe quelle forme arbitraire). Il gère pour vous toutes les règles complexes des flux réactifs, tout en vous offrant une liberté totale et une sécurité de type lors de la mise en œuvre de vos étapes (ce qui pourrait être un flux).
Par exemple, tiré de la documentation, une implémentation GraphStage de l'opérateur filter(T => Boolean)
:
class Filter[A](p: A => Boolean) extends GraphStage[FlowShape[A, A]] {
val in = Inlet[A]("Filter.in")
val out = Outlet[A]("Filter.out")
val shape = FlowShape.of(in, out)
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic =
new GraphStageLogic(shape) {
setHandler(in, new InHandler {
override def onPush(): Unit = {
val elem = grab(in)
if (p(elem)) Push(out, elem)
else pull(in)
}
})
setHandler(out, new OutHandler {
override def onPull(): Unit = {
pull(in)
}
})
}
}
Il gère également les canaux asynchrones et est fusible par défaut.
En plus de la documentation, ces articles de blog expliquent en détail pourquoi cette API est le saint graal de la création d'étapes personnalisées de toutes les formes:
La solution de Konrad montre comment créer une scène personnalisée utilisant des acteurs, mais dans la plupart des cas, je pense que c'est un peu excessif.
Habituellement, vous avez un acteur capable de répondre aux questions:
val actorRef : ActorRef = ???
type Input = ???
type Output = ???
val queryActor : Input => Future[Output] =
(actorRef ? _) andThen (_.mapTo[Output])
Ceci peut être facilement utilisé avec la fonctionnalité Flow
de base qui accepte le nombre maximal de requêtes simultanées:
val actorQueryFlow : Int => Flow[Input, Output, _] =
(parallelism) => Flow[Input].mapAsync[Output](parallelism)(queryActor)
Maintenant, actorQueryFlow
peut être intégré à n'importe quel flux ...
Voici une solution construite en utilisant une étape de graphe. L'acteur doit accuser réception de tous les messages pour exercer une pression en retour. L'acteur est averti lorsque le flux échoue/est terminé et échoue lorsque l'acteur se termine . Cela peut être utile si vous ne souhaitez pas utiliser ask, par exemple lorsque tous les messages d’entrée n’ont pas un message de sortie correspondant.
import akka.actor.{ActorRef, Status, Terminated}
import akka.stream._
import akka.stream.stage.{GraphStage, GraphStageLogic, InHandler, OutHandler}
object ActorRefBackpressureFlowStage {
case object StreamInit
case object StreamAck
case object StreamCompleted
case class StreamFailed(ex: Throwable)
case class StreamElementIn[A](element: A)
case class StreamElementOut[A](element: A)
}
/**
* Sends the elements of the stream to the given `ActorRef` that sends back back-pressure signal.
* First element is always `StreamInit`, then stream is waiting for acknowledgement message
* `ackMessage` from the given actor which means that it is ready to process
* elements. It also requires `ackMessage` message after each stream element
* to make backpressure work. Stream elements are wrapped inside `StreamElementIn(elem)` messages.
*
* The target actor can emit elements at any time by sending a `StreamElementOut(elem)` message, which will
* be emitted downstream when there is demand.
*
* If the target actor terminates the stage will fail with a WatchedActorTerminatedException.
* When the stream is completed successfully a `StreamCompleted` message
* will be sent to the destination actor.
* When the stream is completed with failure a `StreamFailed(ex)` message will be send to the destination actor.
*/
class ActorRefBackpressureFlowStage[In, Out](private val flowActor: ActorRef) extends GraphStage[FlowShape[In, Out]] {
import ActorRefBackpressureFlowStage._
val in: Inlet[In] = Inlet("ActorFlowIn")
val out: Outlet[Out] = Outlet("ActorFlowOut")
override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = new GraphStageLogic(shape) {
private lazy val self = getStageActor {
case (_, StreamAck) =>
if(firstPullReceived) {
if (!isClosed(in) && !hasBeenPulled(in)) {
pull(in)
}
} else {
pullOnFirstPullReceived = true
}
case (_, StreamElementOut(elemOut)) =>
val elem = elemOut.asInstanceOf[Out]
emit(out, elem)
case (_, Terminated(targetRef)) =>
failStage(new WatchedActorTerminatedException("ActorRefBackpressureFlowStage", targetRef))
case (actorRef, unexpected) =>
failStage(new IllegalStateException(s"Unexpected message: `$unexpected` received from actor `$actorRef`."))
}
var firstPullReceived: Boolean = false
var pullOnFirstPullReceived: Boolean = false
override def preStart(): Unit = {
//initialize stage actor and watch flow actor.
self.watch(flowActor)
tellFlowActor(StreamInit)
}
setHandler(in, new InHandler {
override def onPush(): Unit = {
val elementIn = grab(in)
tellFlowActor(StreamElementIn(elementIn))
}
override def onUpstreamFailure(ex: Throwable): Unit = {
tellFlowActor(StreamFailed(ex))
super.onUpstreamFailure(ex)
}
override def onUpstreamFinish(): Unit = {
tellFlowActor(StreamCompleted)
super.onUpstreamFinish()
}
})
setHandler(out, new OutHandler {
override def onPull(): Unit = {
if(!firstPullReceived) {
firstPullReceived = true
if(pullOnFirstPullReceived) {
if (!isClosed(in) && !hasBeenPulled(in)) {
pull(in)
}
}
}
}
override def onDownstreamFinish(): Unit = {
tellFlowActor(StreamCompleted)
super.onDownstreamFinish()
}
})
private def tellFlowActor(message: Any): Unit = {
flowActor.tell(message, self.ref)
}
}
override def shape: FlowShape[In, Out] = FlowShape(in, out)
}