I've been banging my head against the wall for quite some time as I can't figure out how to add an error flow for an akka http websocket flow. What I'm trying to achieve is:
Message comes in from WS client
It's parsed with circe from json
If the message was the right format send the parsed message to an actor
If the message was the wrong format return an error message to the client
The actor can additionally send messages to the client
Without the error handling this was quite easy, but I can't figure out how to add the errors. Here's what I have:
type GameDecodeResult =
Either[(String, io.circe.Error), GameLobby.LobbyRequest]
val errorFlow =
Flow[GameDecodeResult]
.mapConcat {
case Left(err) => err :: Nil
case Right(_) => Nil
}
.map { case (message, error) =>
logger.info(s"failed to parse message $message", error)
TextMessage(Error(error.toString).asJson.spaces2)
}
val normalFlow = {
val normalFlowSink =
Flow[GameDecodeResult]
.mapConcat {
case Right(msg) => msg :: Nil
case Left(_) => Nil
}
.map(req => GameLobby.IncomingMessage(userId, req))
.to(Sink.actorRef[GameLobby.IncomingMessage](gameLobby, PoisonPill))
val normalFlowSource: Source[Message, NotUsed] =
Source.actorRef[GameLobby.OutgoingMessage](10, OverflowStrategy.fail)
.mapMaterializedValue { outActor =>
gameLobby ! GameLobby.UserConnected(userId, outActor)
NotUsed
}
.map(outMessage => TextMessage(Ok(outMessage.message).asJson.spaces2))
Flow.fromSinkAndSource(normalFlowSink, normalFlowSource)
}
val incomingMessageParser =
Flow[Message]
.flatMapConcat {
case tm: TextMessage =>
tm.textStream
case bm: BinaryMessage =>
bm.dataStream.runWith(Sink.ignore)
Source.empty }
.map { message =>
decode[GameLobby.LobbyRequest](message).left.map(err => message -> err)
}
These are my flows defined and I think this should bee good enough, but I have no idea how to assemble them and the complexity of the akka streaming API doesn't help. Here's what I tried:
val x: Flow[Message, Message, NotUsed] =
GraphDSL.create(incomingMessageParser, normalFlow, errorFlow)((_, _, _)) { implicit builder =>
(incoming, normal, error) =>
import GraphDSL.Implicits._
val partitioner = builder.add(Partition[GameDecodeResult](2, {
case Right(_) => 0
case Left(_) => 1
}))
val merge = builder.add(Merge[Message](2))
incoming.in ~> partitioner ~> normal ~> merge
partitioner ~> error ~> merge
}
but admittedly I have absolutely no idea how GraphDSL.create works, where I can use the ~> arrow or what I'm doing in genreal at the last part. It just won't type check and the error messages are not helping me one bit.
A few things needing to be fixed in the Flow you're building using the GraphDSL:
There is no need to pass the 3 subflows to the GraphDSL.create method, as this is only needed to customize the materialized value of your graph. You have already decided the materialized value of your graph is going to be NotUsed.
When connecting incoming using the ~> operator, you need to connect its outlet (.out) to the partition stage.
Every GraphDSL definition block needs to return the shape of your graph - i.e. its external ports. You do that by returning a FlowShape that has incoming.in as input, as merge.out as output. These will define the blueprint of your custom flow.
Because in the end you want to obtain a Flow, you're missing a last call to create is from the graph you defined. This call is Flow.fromGraph(...).
Code example below:
val x: Flow[Message, Message, NotUsed] =
Flow.fromGraph(GraphDSL.create() { implicit builder =>
import GraphDSL.Implicits._
val partitioner = builder.add(Partition[GameDecodeResult](2, {
case Right(_) => 0
case Left(_) => 1
}))
val merge = builder.add(Merge[Message](2))
val incoming = builder.add(incomingMessageParser)
incoming.out ~> partitioner
partitioner ~> normalFlow ~> merge
partitioner ~> errorFlow ~> merge
FlowShape(incoming.in, merge.out)
})
Related
I have a stream with following structure
val source = Source(1 to 10)
val flow1 = Flow[Int].mapAsyncUnordered(2){ x =>
if (x != 7) Future.successful(x)
else Future.failed(new Exception(s"x has failed"))
val flow2 = Flow[Int].mapAsyncUnordered(2){ x =>
if (x != 4) Future.successful(x)
else Future.failed(new Exception(s"x has failed"))
val sink = Sink.fold(List[Int])((xs, x: Int) => x :: xs)
val errorSink = Sink.fold(List[Exception])((errs ,err: Exception) => err :: errs)
My question:
How should I construct the divertTo function to send all exceptions to errorSink?
Any suggestion on how to get the error object with information on which stage it failed would be helpful.
I would recommend modelling your errors as a proper type so that you have a Flow[Either[CustomErrorType, Int]] for instance and then you can use divertTo with a predicate that looks at whether you have a Left or Right.
Or maybe use recover in combination.
See this interesting article: https://bszwej.medium.com/akka-streams-error-handling-7ff9cc01bc12
Future encodes both asynchronicity and can-fail. You'll need to separate the asynchronicity and can-fail.
Try, for instance, is an encoding of can-fail.
Meanwhile, mapAsyncUnordered only emits successes (you can use a supervision strategy to decide not to fail on a failed future, but that will drop the failures not emit them).
It seems that you want to accumulate a list of failures (given the use of Sink.fold). Since that list of failures is only accessible to the outside world through the materialized value, you'll want to use divertToMat instead of divertTo.
From this, the logical solution is:
import scala.concurrent.ExecutionContext
import scala.util.{ Failure, Success, Try }
// Returns a future which is only a failure on fatal exceptions
def liftToFutTry[T](fut: Future[T])(implicit ec: ExecutionContext): Future[Try[T]] =
fut.map(Success(_))
.recoverWith {
case ex => Future.successful(Failure(ex))
}
// for some reason we want a List[Exception] rather than List[Throwable]
val errorSink: Sink[Try[Int], Future[List[Exception]]] =
Flow[Try[Int]]
.mapConcat { t =>
t.failed.get match {
case ex: Exception => List(ex)
case _ => Nil
} : List[Exception]
}
.toMat(Sink.fold(List.empty[Exception]) { (exes, ex) => ex :: exes })(Keep.right)
// materializes as a future of the exceptions which failed in mapAsyncUnordered
val flow1: Flow[Int, Int, Future[List[Exception]]] =
Flow[Int]
.mapAsyncUnordered(2) { x =>
val fut =
if (x != 7) Future.successful(x)
else Future.failed(new Exception(s"$x has failed (equaled 7)"))
liftToFutTry(fut)
}
.divertToMat(errorSink, _.isFailure)(Keep.right) // propagate the failures
.map { successfulTry => successfulTry.get }
If you have two Flows like this and you want to compose them, you'd do
// materialized value is (list of failures from flow1, list of failures from otherFlow
val both: Flow[Int, Int, (Future[List[Exception]], Future[List[Exception]])]
flow1
.viaMat(flow2)(Keep.both)
// materialized value is:
// (
// (
// list of failures from flow1,
// list of failures from otherFlow
// ),
// list of ints which passed through both flow1 and otherFlow
// )
both.toMat(sink)(Keep.both) : Sink[Int, ((Future[List[Exception]], Future[List[Exception]]), Future[List[Int]])]
There are other ways to encode can-fail: e.g. you could use Either from the standard library.
Accumulating a Future[List[_]] may be questionable; note that the Futures won't be complete until the stream finishes.
I'm working with Akka Streams and trying to use Flow stages for the most efficient way to describe an entire graph. In some of the stages I send messages to actors via the ask pattern.
Of course, when I use the ask pattern, I need to use mapTo in order to get the expected type for further processing.
Here is an example:
val runnableGraph = Source.single(CheckEntity(entity))
.map { check =>
(postgresActor ? check)
.mapTo[CheckEntityResult]
.map {
case failure: PostgresFailure => Left(failure.message)
case pEntity: PEntity => Right(check)
}
}
.map {
_.map {
case Left(msg) => Future(Left(msg))
case Right(check) =>
(redisActor ? pEntity)
.mapTo[CheckEntityResult]
.map {
case failure: RedisFailure => Left(failure.message)
case rEntity: REntity => Right(rEntity)
}
}
}
.toMat(Sink.head)(Keep.right)
//The result's type is Future[Future[Either[String, Entity]]]
val futureResult = runnableGraph.run()
How do I get rid of the nested Future between the stages?
One idea to make it easier to propagate a CheckEntity element through the stream is to change your CheckEntityResult class to contain the corresponding CheckEntity instance. This would look something like this:
abstract class CheckEntityResult(entity: CheckEntity) extends Entity
case class PEntity(entity: CheckEntity) extends CheckEntityResult(entity)
case class PostgresFailure(entity: CheckEntity, message: String) extends CheckEntityResult(entity)
case class REntity(entity: CheckEntity) extends CheckEntityResult(entity)
case class RedisFailure(entity: CheckEntity, message: String) extends CheckEntityResult(entity)
Then, after adjusting your actors to handle these messages, you could use Source # ask and mapAsync (adjust the levels of parallelism as needed) to interact with the actors and to avoid the nested Future in the materialized value:
implicit val askTimeout = Timeout(5.seconds)
val runnableGraph = Source.single(CheckEntity(entity))
.ask[CheckEntityResult](parallelism = 3)(postgresActor)
.map {
case PostgresFailure(_, msg) => msg
case PEntity(e) => e
}
.mapAsync(parallelism = 3) {
case failureMsg: String => Future.successful(failureMsg)
case e: CheckEntity => (redisActor ? e).mapTo[CheckEntityResult]
}
.map {
case failureMsg: String => Left(failureMsg)
case RedisFailure(_, msg) => Left(msg)
case r: REntity => Right(r)
}
.toMat(Sink.head)(Keep.right)
val futureResult = runnableGraph.run() // Future[Either[String, Entity]]
You can consider converting your actor query into Flow along with mapAsync (with appropriate parallelism):
val postgresCheck = (check: CheckEntity) =>
(postgresActor ? check).mapTo[CheckEntityResult]
.map {
case failure: PostgresFailure => Left(failure.message)
case pEntity: PEntity => Right(check)
}
val redisCheck = (e: Either[String, CheckEntityResult]) => e match {
case Left(msg) => Future(Left(msg))
case Right(checkResult) =>
(redisActor ? checkResult).mapTo[CheckEntityResult]
.map {
case failure: RedisFailure => Left(failure.message)
case rEntity: REntity => Right(rEntity)
}
}
val postgresCheckFlow = (parallelism: Int) =>
Flow[CheckEntity]
.mapAsync[Either[String, CheckEntityResult]](parallelism)(postgresCheck)
val redisCheckFlow = (parallelism: Int) =>
Flow[Either[String, CheckEntityResult]]
.mapAsync[Either[String, CheckEntityResult]](parallelism)(redisCheck)
With the converted flows, your runnableGraph can be assembled as below with result type Future[Either[]]:
val runnableGraph = Source.single(CheckEntity(entity))
.via(postgresCheckFlow(parallelism))
.via(redisCheckFlow(parallelism))
...
I'd like to use akka streams in order to pipe some json webservices together. I'd like to know the best approach to make a stream from an http request and stream chunks to another.
Is there a way to define such a graph and run it instead of the code below?
So far I tried to do it this way, not sure if it is actually really streaming yet:
override def receive: Receive = {
case GetTestData(p, id) =>
// Get the data and pipes it to itself through a message as recommended
// https://doc.akka.io/docs/akka-http/current/client-side/request-level.html
http.singleRequest(HttpRequest(uri = uri.format(p, id)))
.pipeTo(self)
case HttpResponse(StatusCodes.OK, _, entity, _) =>
val initialRes = entity.dataBytes.via(JsonFraming.objectScanner(Int.MaxValue)).map(bStr => ChunkStreamPart(bStr.utf8String))
// Forward the response to next job and pipes the request response to dedicated actor
http.singleRequest(HttpRequest(
method = HttpMethods.POST,
uri = "googl.cm/flow",
entity = HttpEntity.Chunked(ContentTypes.`application/json`,
initialRes)
))
case resp # HttpResponse(code, _, _, _) =>
log.error("Request to test job failed, response code: " + code)
// Discard the flow to avoid backpressure
resp.discardEntityBytes()
case _ => log.warning("Unexpected message in TestJobActor")
}
This should be a graph equivalent to your receive:
Http()
.cachedHostConnectionPool[Unit](uri.format(p, id))
.collect {
case (Success(HttpResponse(StatusCodes.OK, _, entity, _)), _) =>
val initialRes = entity.dataBytes
.via(JsonFraming.objectScanner(Int.MaxValue))
.map(bStr => ChunkStreamPart(bStr.utf8String))
Some(initialRes)
case (Success(resp # HttpResponse(code, _, _, _)), _) =>
log.error("Request to test job failed, response code: " + code)
// Discard the flow to avoid backpressure
resp.discardEntityBytes()
None
}
.collect {
case Some(initialRes) => initialRes
}
.map { initialRes =>
(HttpRequest(
method = HttpMethods.POST,
uri = "googl.cm/flow",
entity = HttpEntity.Chunked(ContentTypes.`application/json`, initialRes)
),
())
}
.via(Http().superPool[Unit]())
The type of this is Flow[(HttpRequest, Unit), (Try[HttpResponse], Unit), HostConnectionPool], where the Unit is a correlation ID you can use if you want to know which request corresponds to the response arrived, and HostConnectionPool materialized value can be used to shut down the connection to the host. Only cachedHostConnectionPool gives you back this materialized value, superPool probably handles this on its own (though I haven't checked). Anyway, I recommend you just use Http().shutdownAllConnectionPools() upon shutdown of your application unless you need otherwise for some reason. In my experience, it's much less error prone (e.g. forgetting the shutdown).
You can also use Graph DSL, to express the same graph:
val graph = Flow.fromGraph(GraphDSL.create() { implicit b =>
import GraphDSL.Implicits._
val host1Flow = b.add(Http().cachedHostConnectionPool[Unit](uri.format(p, id)))
val host2Flow = b.add(Http().superPool[Unit]())
val toInitialRes = b.add(
Flow[(Try[HttpResponse], Unit)]
.collect {
case (Success(HttpResponse(StatusCodes.OK, _, entity, _)), _) =>
val initialRes = entity.dataBytes
.via(JsonFraming.objectScanner(Int.MaxValue))
.map(bStr => ChunkStreamPart(bStr.utf8String))
Some(initialRes)
case (Success(resp # HttpResponse(code, _, _, _)), _) =>
log.error("Request to test job failed, response code: " + code)
// Discard the flow to avoid backpressure
resp.discardEntityBytes()
None
}
)
val keepOkStatus = b.add(
Flow[Option[Source[HttpEntity.ChunkStreamPart, Any]]]
.collect {
case Some(initialRes) => initialRes
}
)
val toOtherHost = b.add(
Flow[Source[HttpEntity.ChunkStreamPart, Any]]
.map { initialRes =>
(HttpRequest(
method = HttpMethods.POST,
uri = "googl.cm/flow",
entity = HttpEntity.Chunked(ContentTypes.`application/json`, initialRes)
),
())
}
)
host1Flow ~> toInitialRes ~> keepOkStatus ~> toOtherHost ~> host2Flow
FlowShape(host1Flow.in, host2Flow.out)
})
I started playing around scala and came to this particular boilerplate of web socket chatroom in scala.
They use MessageHub.source() and BroadcastHub.sink() as their Source and Sink for sending the messages to all connected clients.
The example is working fine for exchanging messages as it is.
private val (chatSink, chatSource) = {
// Don't log MergeHub$ProducerFailed as error if the client disconnects.
// recoverWithRetries -1 is essentially "recoverWith"
val source = MergeHub.source[WSMessage]
.log("source")
.recoverWithRetries(-1, { case _: Exception ⇒ Source.empty })
val sink = BroadcastHub.sink[WSMessage]
source.toMat(sink)(Keep.both).run()
}
private val userFlow: Flow[WSMessage, WSMessage, _] = {
Flow.fromSinkAndSource(chatSink, chatSource)
}
def chat(): WebSocket = {
WebSocket.acceptOrResult[WSMessage, WSMessage] {
case rh if sameOriginCheck(rh) =>
Future.successful(userFlow).map { flow =>
Right(flow)
}.recover {
case e: Exception =>
val msg = "Cannot create websocket"
logger.error(msg, e)
val result = InternalServerError(msg)
Left(result)
}
case rejected =>
logger.error(s"Request ${rejected} failed same origin check")
Future.successful {
Left(Forbidden("forbidden"))
}
}
}
I want to store the messages that are exchanged in the chatroom in a DB.
I tried adding map and fold functions to source and sink to get hold of the messages that are sent but I wasn't able to.
I tried adding a Flow stage between MergeHub and BroadcastHub like below
val flow = Flow[WSMessage].map(element => println(s"Message: $element"))
source.via(flow).toMat(sink)(Keep.both).run()
But it throws a compilation error that cannot reference toMat with such signature.
Can someone help or point me how can I get hold of messages that are sent and store them in DB.
Link for full template:
https://github.com/playframework/play-scala-chatroom-example
Let's look at your flow:
val flow = Flow[WSMessage].map(element => println(s"Message: $element"))
It takes elements of type WSMessage, and returns nothing (Unit). Here it is again with the correct type:
val flow: Flow[Unit] = Flow[WSMessage].map(element => println(s"Message: $element"))
This will clearly not work as the sink expects WSMessage and not Unit.
Here's how you can fix the above problem:
val flow = Flow[WSMessage].map { element =>
println(s"Message: $element")
element
}
Not that for persisting messages in the database, you will most likely want to use an async stage, roughly:
val flow = Flow[WSMessage].mapAsync(parallelism) { element =>
println(s"Message: $element")
// assuming DB.write() returns a Future[Unit]
DB.write(element).map(_ => element)
}
I'm trying to create an endpoint on my Akka Http Server which tells the users it's IP address using an external service (I know this can be performed way easier but I'm doing this as a challenge).
The code that doesn't make use of streams on the upper most layer is this:
implicit val system = ActorSystem()
implicit val materializer = ActorMaterializer()
val requestHandler: HttpRequest => Future[HttpResponse] = {
case HttpRequest(GET, Uri.Path("/"), _, _, _) =>
Http().singleRequest(HttpRequest(GET, Uri("http://checkip.amazonaws.com/"))).flatMap { response =>
response.entity.dataBytes.runFold(ByteString(""))(_ ++ _) map { string =>
HttpResponse(entity = HttpEntity(MediaTypes.`text/html`,
"<html><body><h1>" + string.utf8String + "</h1></body></html>"))
}
}
case _: HttpRequest =>
Future(HttpResponse(404, entity = "Unknown resource!"))
}
Http().bindAndHandleAsync(requestHandler, "localhost", 8080)
and it is working fine. However, as a challenge, I wanted to limit myself to only using streams (no Future's).
This is the layout I thought I'd use for this kind of an approach:
Source[Request] -> Flow[Request, Request] -> Flow[Request, Response] ->Flow[Response, Response] and to accommodate the 404 route, also Source[Request] -> Flow[Request, Response]. Now, if my Akka Stream knowledge serves me well, I need to use a Flow.fromGraph for such a thing, however, this is where I'm stuck.
In a Future I can do an easy map and flatMap for the various endpoints but in streams that would mean dividing up the Flow into multiple Flow's and I'm not quite sure how I'd do that. I thought about using UnzipWith and Options or a generic Broadcast.
Any help on this subject would be much appreciated.
I don't if this would be necessary? -- http://doc.akka.io/docs/akka-stream-and-http-experimental/2.0-M2/scala/stream-customize.html
You do not need to use Flow.fromGraph. Instead, a singular Flow that uses flatMapConcat will work:
//an outgoing connection flow
val checkIPFlow = Http().outgoingConnection("checkip.amazonaws.com")
//converts the final html String to an HttpResponse
def byteStrToResponse(byteStr : ByteString) =
HttpResponse(entity = new Default(MediaTypes.`text/html`,
byteStr.length,
Source.single(byteStr)))
val reqResponseFlow = Flow[HttpRequest].flatMapConcat[HttpResponse]( _ match {
case HttpRequest(GET, Uri.Path("/"), _, _, _) =>
Source.single(HttpRequest(GET, Uri("http://checkip.amazonaws.com/")))
.via(checkIPFlow)
.mapAsync(1)(_.entity.dataBytes.runFold(ByteString(""))(_ ++ _))
.map("<html><body><h1>" + _.utf8String + "</h1></body></html>")
.map(ByteString.apply)
.map(byteStrToResponse)
case _ =>
Source.single(HttpResponse(404, entity = "Unknown resource!"))
})
This Flow can then be used to bind to incoming requests:
Http().bindAndHandle(reqResponseFlow, "localhost", 8080)
And all without Futures...