4

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.

Community
  • 1
  • 1
Chris W.
  • 2,266
  • 20
  • 40
  • 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. – 4lex1v Jun 27 '14 at 13:37
  • Thanks AlexIv, I know that, but this wasn't my question, see my comment to your answer below – Chris W. Jun 30 '14 at 07:29

3 Answers3

3

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.

stempler
  • 750
  • 6
  • 15
  • I don't see any implicit conversions in your example..?? – sweepy84 Nov 01 '16 at 12:01
  • @sweepy84 The code only consists of the classes, the implicit conversion is defined elsewhere (I did it in a package object - needs to be imported) and could look like this: `implicit def future2RichFuture[T](future: Future[T]) = new RichFuture(future)` – stempler Nov 01 '16 at 16:16
  • The implicit def could be avoided by making `RichFuture` an implicit class: `implicit class RichFuture[+T](val future: Future[T]) extends AnyVal { def asLogged(implicit ec: ExecutionContext): Future[T] = LoggedFuture(future) }` – Bruno Oct 21 '17 at 23:38
1

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
 }
Bruno
  • 854
  • 7
  • 21
0

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.

4lex1v
  • 21,367
  • 6
  • 52
  • 86
  • Thanks AlexIv, I've already got that point. What I've been asking for was only a simple method to always use futures with a given failure handler. When I'm using map() e.g. I have to re-add the handler for failures that may occur in the code inside the map-function. – Chris W. Jun 30 '14 at 07:27
  • @longliveenduro again, no, this is the point you didn't get. Take a look at my example, it throws `TestError` inside `map`, but if `f1` contains a failure, `f2` won't contain `TestError`, but `NumberError`, so no need it attaching more failure handler to further futures, cause they won't happen – 4lex1v Jun 30 '14 at 07:33
  • Yes what you say is absolutely correct but unfortunately doesn't address what I've asked. I've found a very similiar discussion here: http://grokbase.com/t/gg/scala-user/1419gxgzza/the-silence-of-failed-futures – Chris W. Jun 30 '14 at 10:16