Scala Futures: Default error handler for every new created, or mapped exception

为君一笑 提交于 2019-12-19 22:10:32

问题


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 a Future and onFailure logs the exception similar to @LimbSoups answer
  • for methods like map that return a new Future yield a LoggedFuture as well
  • use a Promise as some kind of fail event that is shared between the cascaded LoggedFutures 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

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!