问题
Is there any possibility to always create a Future{...} block with an default onFailure handler? (e.g. write the stacktrace to the console)? This handler should also be automatically attached to mapped futures (new futures created by calling map on an future already having a default failure handler)
See also my question here for more details: Scala on Android with scala.concurrent.Future do not report exception on system err/out
I want to have a "last resort" exception logging code, if someone does not use onFailure or sth similar on a returned future.
回答1:
I had a similar problem, futures failing silently in cases where the actual result is irrelevant and thus not handled explicitly. From the documentation in ExecutionContext
I initially assumed that the reportFailure
method there was to do reporting for any failure in a Future
. Which is obviously wrong - so this is the approach I came up with to have logged exceptions (even for mapped or otherwise derived) futures:
- a
LoggedFuture
class that delegates to aFuture
andonFailure
logs the exception similar to @LimbSoups answer - for methods like
map
that return a newFuture
yield aLoggedFuture
as well - use a
Promise
as some kind of fail event that is shared between the cascadedLoggedFutures
to log an exception only once even if the onFailure callback is applied multiple times because of the propagation
object LoggedFuture {
def apply[T](future: Future[T])(implicit ec: ExecutionContext): Future[T] = {
if (future.isInstanceOf[LoggedFuture[T]]) {
// don't augment to prevent double logging
future.asInstanceOf[LoggedFuture[T]]
}
else {
val failEvent = promise[Unit]
failEvent.future.onFailure {
// do your actual logging here
case t => t.printStackTrace()
}
new LoggedFuture(future, failEvent, ec)
}
}
}
private class LoggedFuture[T](future: Future[T], failEvent: Promise[Unit], ec: ExecutionContext) extends Future[T] {
// fire "log event" on failure
future.onFailure {
// complete log event promise
// the promise is used to log the error only once, even if the
// future is mapped and thus further callbacks attached
case t => failEvent.tryComplete(Failure(t))
} (ec)
// delegate methods
override def ready(atMost: Duration)(implicit permit: CanAwait): this.type = {
future.ready(atMost)
this
}
override def result(atMost: scala.concurrent.duration.Duration)(implicit permit: CanAwait): T = future.result(atMost)
override def isCompleted: Boolean = future.isCompleted
override def onComplete[U](func: scala.util.Try[T] => U)(implicit executor: ExecutionContext): Unit = future.onComplete(func)
override def value: Option[Try[T]] = future.value
// propagate LoggedFuture (and shared log event) whenever a new future is returned
override def map[S](f: T => S)(implicit executor: ExecutionContext): Future[S] =
new LoggedFuture(super.map(f), failEvent, executor)
override def transform[S](s: T => S, f: Throwable => Throwable)(implicit executor: ExecutionContext): Future[S] =
new LoggedFuture(super.transform(s, f), failEvent, executor)
override def flatMap[S](f: T => Future[S])(implicit executor: ExecutionContext): Future[S] =
new LoggedFuture(super.flatMap(f), failEvent, executor)
override def recover[U >: T](pf: PartialFunction[Throwable, U])(implicit executor: ExecutionContext): Future[U] =
new LoggedFuture(super.recover(pf), failEvent, executor)
override def recoverWith[U >: T](pf: PartialFunction[Throwable, Future[U]])(implicit executor: ExecutionContext): Future[U] =
new LoggedFuture(super.recoverWith(pf), failEvent, executor)
override def zip[U](that: Future[U]): Future[(T, U)] =
new LoggedFuture(super.zip(that), failEvent, ec)
override def fallbackTo[U >: T](that: Future[U]): Future[U] =
new LoggedFuture(super.fallbackTo(that), failEvent, ec)
override def andThen[U](pf: PartialFunction[Try[T], U])(implicit executor: ExecutionContext): Future[T] =
new LoggedFuture(super.andThen(pf), failEvent, executor)
}
class RichFuture[T](future: Future[T]) {
def asLogged(implicit ec: ExecutionContext): Future[T] = LoggedFuture(future)
}
Additionally, I have an implicit conversion to RichFuture
(as above) defined so I can easily convert existing futures with calls like future.asLogged
.
回答2:
With the following implicit class, you can easily log failures of your futures while avoiding the boilerplate of recover
:
import com.typesafe.scalalogging.Logger
implicit class LoggingFuture[+T](val f: Future[T]) extends AnyVal {
def withFailureLogging(l: Logger, message: String): Future[T] = f recover {
case e =>
l.error(s"$message: $e")
throw e
}
def withPrintStackTraceOnFailure: Future[T] = f recover {
case e =>
e.printStackTrace()
throw e
}
}
You can use it as shown below:
import com.typesafe.scalalogging._
import scala.language.postfixOps
class MyClass extends LazyLogging {
def f = Future {
// do something that fails
throw new Exception("this future fails")
} withFailureLogging(logger, "some error message")
def g = Future {
// do something that fails
throw new Exception("this future fails")
} withPrintStackTraceOnFailure
}
回答3:
Like an extension to my comment:
You didn't get the point, there is no need in making failure callbacks for each mapped future, cause in case of failure map won't do any computations, just pass existing failure further. So if you chained more computations to the failed one, all new callbacks just won't be called.
Consider this example:
case class TestError(msg) extends Throwable(msg)
val f1 = Future { 10 / 0 }
val f2 = f1 map { x => throw new TestError("Hello"); x + 10 }
f1.onFailure {
case error => println(error.getMessage)
}
f2.onFailure {
case er: TestError => println("TestError")
case _ => println("Number error")
}
// Exiting paste mode, now interpreting.
/ by zero
Number error
f1: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@54659bf8
f2: scala.concurrent.Future[Int] = scala.concurrent.impl.Promise$DefaultPromise@5ae2e211
As you can see the first callback print the error message and the second ignores thrown TestError
. That's because you map function doesn't applied. If you take a look at the comment to the map
:
/** Creates a new future by applying a function to the successful result of
* this future. If this future is completed with an exception then the new
* future will also contain this exception.
*/
So there is no need in attaching new failure callbacks further, cause any further future would simple contain the result of the previous one, for each you've already defined a callback.
来源:https://stackoverflow.com/questions/24453080/scala-futures-default-error-handler-for-every-new-created-or-mapped-exception