Reputation: 1989
I write to a database in a simple function that returns a Future of the list that has been written to the database.
def writeToDB(db:Database, items: List[(String,String)]) = {
Future {
db.writeAsync(items) // Returns Unit
items
}
}
val res: Future[List[(String,String)]] = writeToDB(someItems)
On Success
I have access to the written items so I can report which items have been written. But on Failure
, in case the database fails, I don't have access to the failed items (e.g. for retry, for reporting and ...).
Is there any way to capture the items
in error, something like Either
that has Success(items)
and Failure(items)
?
Upvotes: 0
Views: 177
Reputation: 20591
Because Scala's Future
is essentially an asynchronous Try
, failure has to be encoded as a Throwable
(most likely as an Exception
). One way to encode the failure to write an item to the DB is via
case class DBFailedToWriteItem(items: Seq[(String, String)], cause: Throwable) extends Exception("Failed to write item to database", cause)
def writeToDB(db: Database, items: Seq[(String, String)]): Future[Seq[(String, String)]] =
Future { db.writeAsync(items) }
.map { _ => items }
.recoverWith {
case NonFatal(e) => Future.failed(DBFailedToWriteItem(items, e))
}
.recoverWith
allows you to transform a failed Future
into an arbitrary (either failed or successful) Future
. The scala.util.control.NonFatal
extractor prevents it from transforming certain failures which it's not reasonable to transform (generally ones that indicate the JVM is irretrievably hosed); I forget if recoverWith
and friends implicitly perform this exclusion, so the match might be a bit of "belt and suspenders".
That said, I'm curious regarding the interface exposed by Database
. If db.writeAsync
is async, it's kind of odd for it to return Unit
without giving the opportunity to attach a callback. The API seems to imply that it will catch a failure to set up the write, but not a failure to complete the write.
EDIT to add: from a comment, db.writeAsync
returns a CompletableFuture
(which is also a CompletionStage
, Java's future API does not observe the read/write separation that Scala's does). You can use scala-java8-compat's scala.compat.java8.FutureConverters
to convert a CompletionStage
into a Future
.
sealed trait DBFailed
def writeToDB(db: Database, items: Seq[(String, String)]): Future[Either[DBFailed, Seq[(String, String)]]] = {
import scala.compat.java8.FutureConverters.toScala
toScala(db.writeAsync(items))
.map { _ => Right(items) }
.recover {
case NonFatal(e) => Left(???) // create an appropriate
// DBFailed... you can also
// pattern match on the
// particular exception from
// db.writeAsync
}
}
Note that with Either
the convention is for the failure case to be the left (first) subtype and the success to be the right. Since Scala 2.12, this convention is encouraged by right-biasing Either
:
val either: Either[String, Int] = ???
val rightTransform: Either[String, Double] = either.map(_.toDouble + 42.0)
val leftTransform: Either[Option[Char], Int] = either.left.map( _.headOption)
Upvotes: 4