web-dev-qa-db-fra.com

Créer un flux d'acteur dans Akka Streams

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).

21
Ori Popowski

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:

38

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 ...

19

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)

}
1
Meeuw