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.
Related
I have the following Slick code that given an id returns a customer (if exists). If there's a problem (such as connectivity lost) a Failure clause will throw an exception:
def read (id: Int): Future[Option[Customer]] = {
val db = // ....
val customers = TableQuery[CustomerDB]
val action = customers.filter(_.id === id).result
val future = db.run(action.asTry)
future.map{
case Success(s) =>
if (s.length>0)
Some(s(0))
else
None
case Failure(f) => throw new Exception (f.getMessage)
}
}
Now, my understanding is that instead of using try/catch/finally of exceptions, in Scala one should use Try. In addition, no exceptions should be thrown. But if the exception is not thrown, how to notify the upper layer that a problem occurred?
Future itself does already have Try inside. So, I would say that you need to just flatten (also you code a bit complicated, I simplified):
future.flatMap {
case Success(s) => Future.successful(s.headOption)
case Failure(f) => Future.failed(f)
}
Result Future when in failed state notifies caller that execution failed (with wrapped original exception). Otherwise, successful.
The right way to do report errors is by using Either.
trait Error
case class NotFound(id: Int) extends Error
case class QueryFailed(msg: String) extends Error
def read (id: Int): Future[Either[Error, Customer]] = {
val db = // ....
val customers = TableQuery[CustomerDB]
val action = customers.filter(_.id === id).result
val future = db.run(action.asTry)
future.map{
case Success(s) =>
if (s.length>0)
Right(s(0))
else
Left(NotFound(id))
case Failure(f) => Left(QueryFailed(f.getMessage))
}
}
Ok so, in general you can use Future.successful or Future.failed(msg: String) to "signal" the upper level (aka calling method) you got the value or not.
Better approach
A good approach is however to use .recoverWith{} on a Future in case of failure.
For example:
def getUserFromCloud (userId: String): Future[String] = Future{
cloudProviderApi.getUsername(userId)
}.recoverWith{
Future.failed(s"$userId does not exist.")
}
What about the calling method?
Well you just map the success with and underscode and deal with the error by using recover:
getUserFromCloud("test").map(_ => {
//In case of success
}).recover{
//In case of failure, like return BadRequest.
}
More on recover and recoverWith in case you are interested: Scala recover or recoverWith
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)
})
Consider the following code inside a Play Framework controller:
val firstFuture = function1(id)
val secondFuture = function2(id)
val resultFuture = for {
first <- firstFuture
second <- secondFuture(_.get)
result <- function3(first, second)
} yield Ok(s"Processed $id")
resultFuture.map(result => result).recover { case t => InternalServerError(s"Error organizing files: $t.getMessage")}
Here are some details about the functions:
function1 returns Future[List]
function2 returns Future[Option[Person]]
function1 and function2 can run in parallel, but function3 needs the results for both.
Given this information, I have some questions:
Although the application is such that this code is very unlikely to be called with an improper id, I would like to handle this possibility. Basically, I would like to return NotFound if function2 returns None, but I can't figure out how to do that.
Will the recover call handle an Exception thrown any step of the way?
Is there a more elegant or idiomatic way to write this code?
Perhaps using collect, and then you can recover the NoSuchElementException--which yes, will recover a failure from any step of the way. resultFuture will either be successful with the mapped Result, or failed with the first exception that was thrown.
val firstFuture = function1(id)
val secondFuture = function2(id)
val resultFuture = for {
first <- firstFuture
second <- secondFuture.collect(case Some(x) => x)
result <- function3(first, second)
} yield Ok(s"Processed $id")
resultFuture.map(result => result)
.recover { case java.util.NoSuchElementException => NotFound }
.recover { case t => InternalServerError(s"Error organizing files: $t.getMessage")}
I would go with Scalaz OptionT. Maybe when you have only one function Future[Optipn[T]] it's overkill, but when you'll start adding more functions it will become super useful
import scala.concurrent.ExecutionContext.Implicits.global
import scalaz.OptionT
import scalaz.OptionT._
import scalaz.std.scalaFuture._
// Wrap 'some' result into OptionT
private def someOptionT[T](t: Future[T]): OptionT[Future, T] =
optionT[Future](t.map(Some.apply))
val firstFuture = function1(id)
val secondFuture = function2(id)
val action = for {
list <- someOptionT(firstFuture)
person <- optionT(secondFuture)
result = function3(list, person)
} yield result
action.run.map {
case None => NotFound
case Some(result) => Ok(s"Processed $id")
} recover {
case NonFatal(err) => InternalServerError(s"Error organizing files: ${err.getMessage}")
}
I have the following code snippet that I use to read a record from the database and I'm using ReactiveMongo for this.
val futureList: Future[Option[BSONDocument]] = collection.find(query).cursor[BSONDocument].headOption
val os: Future[Option[Exam]] = futureList.map {
(list: Option[BSONDocument]) => list match {
case Some(examBSON) => {
val id = examBSON.getAs[Int]("id").get
val text = examBSON.getAs[String]("text").get
val description = examBSON.getAs[String]("description").get
val totalQuestions = examBSON.getAs[Int]("totalQuestions").get
val passingScore = examBSON.getAs[Int]("passingScore").get
Some(Exam(id, text, description, totalQuestions, passingScore))
}
case None => None
}
}.recover {
case t: Throwable => // Log exception
None
}
I do not want to change my method signature to return a Future. I want to get the value inside the Future and return it to the caller.
You need then to block using the awaitable object:
import scala.concurrent.duration._
val os: Future[Option[Exam]] = ???
val result = Await.result(os, 10 seconds)
result.getOrElse(/* some default */)
Note that blocking will block the thread until the future is completed or the timeout expires and an exception is thrown, note also that this kinda defeats the purpose of having async computation, but it may be ok depending on your use case.
If you don't need the result immediately you can attach a callback using onComplete
os onComplete {
case Success(someOption) => myMethod(someOption)
case Failure(t) => println("Error)
}
Note that onComplete will be fired only when the future is completed so the result is not immediately accessible, also the return type is Unit.
I am trying to create a neat construction with for-comprehension for business logic built on futures. Here is a sample which contains a working example based on Exception handling:
(for {
// find the user by id, findUser(id) returns Future[Option[User]]
userOpt <- userDao.findUser(userId)
_ = if (!userOpt.isDefined) throw new EntityNotFoundException(classOf[User], userId)
user = userOpt.get
// authenticate it, authenticate(user) returns Future[AuthResult]
authResult <- userDao.authenticate(user)
_ = if (!authResult.ok) throw new AuthFailedException(userId)
// find the good owned by the user, findGood(id) returns Future[Option[Good]]
goodOpt <- goodDao.findGood(goodId)
_ = if (!good.isDefined) throw new EntityNotFoundException(classOf[Good], goodId)
good = goodOpt.get
// check ownership for the user, checkOwnership(user, good) returns Future[Boolean]
ownership <- goodDao.checkOwnership(user, good)
if (!ownership) throw new OwnershipException(user, good)
_ <- goodDao.remove(good)
} yield {
renderJson(Map(
"success" -> true
))
})
.recover {
case ex: EntityNotFoundException =>
/// ... handle error cases ...
renderJson(Map(
"success" -> false,
"error" -> "Your blahblahblah was not found in our database"
))
case ex: AuthFailedException =>
/// ... handle error cases ...
case ex: OwnershipException =>
/// ... handle error cases ...
}
However this might be seen as a non-functional or non-Scala way to handle the things. Is there a better way to do this?
Note that these errors come from different sources - some are at the business level ('checking ownership') and some are at controller level ('authorization') and some are at db level ('entity not found'). So approaches when you derive them from a single common error type might not work.
Don't use exceptions for expected behaviour.
It's not nice in Java, and it's really not nice in Scala. Please see this question for more information about why you should avoid using exceptions for regular control flow. Scala is very well equipped to avoid using exceptions: you can use Eithers.
The trick is to define some failures you might encounter, and convert your Options into Eithers that wrap these failures.
// Failures.scala
object Failures {
sealed trait Failure
// Four types of possible failures here
case object UserNotFound extends Failure
case object NotAuthenticated extends Failure
case object GoodNotFound extends Failure
case object NoOwnership extends Failure
// Put other errors here...
// Converts options into Eithers for you
implicit class opt2either[A](opt: Option[A]) {
def withFailure(f: Failure) = opt.fold(Left(f))(a => Right(a))
}
}
Using these helpers, you can make your for comprehension readable and exception free:
import Failures._
// Helper function to make ownership checking more readable in the for comprehension
def checkGood(user: User, good: Good) = {
if(checkOwnership(user, good))
Right(good)
else
Left(NoOwnership)
}
// First create the JSON
val resultFuture: Future[Either[Failure, JsResult]] = for {
userRes <- userDao.findUser(userId)
user <- userRes.withFailure(UserNotFound).right
authRes <- userDao.authenticate(user)
auth <- authRes.withFailure(NotAuthenticated).right
goodRes <- goodDao.findGood(goodId)
good <- goodRes.withFailure(GoodNotFound).right
checkedGood <- checkGood(user, good).right
} yield renderJson(Map("success" -> true)))
// Check result and handle any failures
resultFuture.map { result =>
result match {
case Right(json) => json // serve json
case Left(failure) => failure match {
case UserNotFound => // Handle errors
case NotAuthenticated =>
case GoodNotFound =>
case NoOwnership =>
case _ =>
}
}
}
You could clean up the for comprehension a little to look like this:
for {
user <- findUser(userId)
authResult <- authUser(user)
good <- findGood(goodId)
_ <- checkOwnership(user, good)
_ <- goodDao.remove(good)
} yield {
renderJson(Map(
"success" -> true
))
}
Assuming these methods:
def findUser(id:Long) = find(id, userDao.findUser)
def findGood(id:Long) = find(id, goodDao.findGood)
def find[T:ClassTag](id:Long, f:Long => Future[Option[T]]) = {
f(id).flatMap{
case None => Future.failed(new EntityNotFoundException(implicitly[ClassTag[T]].runtimeClass, id))
case Some(entity) => Future.successful(entity)
}
}
def authUser(user:User) = {
userDao.authenticate(user).flatMap{
case result if result.ok => Future.failed(new AuthFailedException(userId))
case result => Future.successful(result)
}
}
def checkOwnership(user:User, good:Good):Future[Boolean] = {
val someCondition = true //real logic for ownership check goes here
if (someCondition) Future.successful(true)
else Future.failed(new OwnershipException(user, good))
}
The idea here is to use flatMap to turn things like Options that are returned wrapped in Futures into failed Futures when they are None. There are going to be a lot of ways to do clean up that for comp and this is one possible way to do it.
The central challenge is that for-comprehensions can only work on one monad at a time, in this case it being the Future monad and the only way to short-circuit a sequence of future calls is for the future to fail. This works because the subsequent calls in the for-comprehension are just map and flatmap calls, and the behavior of a map/flatmap on a failed Future is to return that future and not execute the provided body (i.e. the function being called).
What you are trying to achieve is the short-cicuiting of a workflow based on some conditions and not do it by failing the future. This can be done by wrapping the result in another container, let's call it Result[A], which gives the comprehension a type of Future[Result[A]]. Result would either contain a result value, or be a terminating result. The challenge is how to:
provide subsequent function calls the value contained by a prior non-terminating Result
prevent the subsequent function call from being evaluated if the Result is terminating
map/flatmap seem like the candidates for doing these types of compositions, except we will have to call them manually, since the only map/flatmap that the for-comprehension can evaluate is one that results in a Future[Result[A]].
Result could be defined as:
trait Result[+A] {
// the intermediate Result
def value: A
// convert this result into a final result based on another result
def given[B](other: Result[B]): Result[A] = other match {
case x: Terminator => x
case v => this
}
// replace the value of this result with the provided one
def apply[B](v: B): Result[B]
// replace the current result with one based on function call
def flatMap[A2 >: A, B](f: A2 => Future[Result[B]]): Future[Result[B]]
// create a new result using the value of both
def combine[B](other: Result[B]): Result[(A, B)] = other match {
case x: Terminator => x
case b => Successful((value, b.value))
}
}
For each call, the action is really a potential action, as calling it on or with a terminating result, will simply maintain the terminating result. Note that Terminator is a Result[Nothing] since it will never contain a value and any Result[+A] can be a Result[Nothing].
The terminating result is defined as:
sealed trait Terminator extends Result[Nothing] {
val value = throw new IllegalStateException()
// The terminator will always short-circuit and return itself as
// the success rather than execute the provided block, thus
// propagating the terminating result
def flatMap[A2 >: Nothing, B](f: A2 => Future[Result[B]]): Future[Result[B]] =
Future.successful(this)
// if we apply just a value to a Terminator the result is always the Terminator
def apply[B](v: B): Result[B] = this
// this apply is a convenience function for returning this terminator
// or a successful value if the input has some value
def apply[A](opt: Option[A]) = opt match {
case None => this
case Some(v) => Successful[A](v)
}
// this apply is a convenience function for returning this terminator or
// a UnitResult
def apply(bool: Boolean): Result[Unit] = if (bool) UnitResult else this
}
The terminating result makes it possible to to short-circuit calls to functions that require a value [A] when we've already met our terminating condition.
The non-terminating result is defined as:
trait SuccessfulResult[+A] extends Result[A] {
def apply[B](v: B): Result[B] = Successful(v)
def flatMap[A2 >: A, B](f: A2 => Future[Result[B]]): Future[Result[B]] = f(value)
}
case class Successful[+A](value: A) extends SuccessfulResult[A]
case object UnitResult extends SuccessfulResult[Unit] {
val value = {}
}
The non-teminating result makes it possible to provide the contained value [A] to functions. For good measure, I've also predefined a UnitResult for functions that are purely side-effecting, like goodDao.removeGood.
Now let's define your good, but terminating conditions:
case object UserNotFound extends Terminator
case object NotAuthenticated extends Terminator
case object GoodNotFound extends Terminator
case object NoOwnership extends Terminator
Now we have the tools to create the the workflow you were looking for. Each for comprehention wants a function that returns a Future[Result[A]] on the right-hand side, producing a Result[A] on the left-hand side. The flatMap on Result[A] makes it possible to call (or short-circuit) a function that requires an [A] as input and we can then map its result to a new Result:
def renderJson(data: Map[Any, Any]): JsResult = ???
def renderError(message: String): JsResult = ???
val resultFuture = for {
// apply UserNotFound to the Option to conver it into Result[User] or UserNotFound
userResult <- userDao.findUser(userId).map(UserNotFound(_))
// apply NotAuthenticated to AuthResult.ok to create a UnitResult or NotAuthenticated
authResult <- userResult.flatMap(user => userDao.authenticate(user).map(x => NotAuthenticated(x.ok)))
goodResult <- authResult.flatMap(_ => goodDao.findGood(goodId).map(GoodNotFound(_)))
// combine user and good, so we can feed it into checkOwnership
comboResult = userResult.combine(goodResult)
ownershipResult <- goodResult.flatMap { case (user, good) => goodDao.checkOwnership(user, good).map(NoOwnership(_))}
// in order to call removeGood with a good value, we take the original
// good result and potentially convert it to a Terminator based on
// ownershipResult via .given
_ <- goodResult.given(ownershipResult).flatMap(good => goodDao.removeGood(good).map(x => UnitResult))
} yield {
// ownership was the last result we cared about, so we apply the output
// to it to create a Future[Result[JsResult]] or some Terminator
ownershipResult(renderJson(Map(
"success" -> true
)))
}
// now we can map Result into its value or some other value based on the Terminator
val jsFuture = resultFuture.map {
case UserNotFound => renderError("User not found")
case NotAuthenticated => renderError("User not authenticated")
case GoodNotFound => renderError("Good not found")
case NoOwnership => renderError("No ownership")
case x => x.value
}
I know that's a whole lot of setup, but at least the Result type can be used for any Future for-comprehension that has terminating conditions.