Quantcast
Channel: Kifi Engineering Blog » Andrew Conner
Viewing all articles
Browse latest Browse all 15

Future goodies and helpers: SafeFuture, TimeoutFuture, CancelableFuture

$
0
0

safety first signRelated to our previous post about catching top-level Actor exceptions, we also have a few utility classes for working with Futures. These do things such as offer baked-in exception logging, establish SLA-timeouts on futures, and give the ability to interrupt a running code block that is in a future.

Check out all of the code together on Github.

SafeFuture

SafeFuture is a Future that simply binds an onFailure to any future passed in to deal with hard failures. Future failures can and should be handled, but for fire-and-forget and map-chaining, it’s often easy to not handle failures explicitly. The result is a exception that goes nowhere — including failure to your go to your logs. Mysterious failure is not good, so SafeFuture lets you design an application level failure monitoring strategy. This won’t save your result, but at least you’ll be aware it failed.

As an added bonus, there’s a second apply in SafeFuture that lets you name the future for logging purposes.

class SafeFuture[+T](future: Future[T], name: Option[String] = None)(implicit executor: ExecutionContext) extends Future[T] {
 
  future match {
    case _: SafeFuture[_] =>
    case dangerousFuture =>
      dangerousFuture.onFailure {
        case cause: Throwable =>
          cause.printStackTrace() // should always work, to stderr
          try {
            // Should work if the Logger is up:
            Logger(getClass).error("[SafeFuture] Failure of future" + name.map(": " + _).getOrElse(""), cause)
 
            // ... and add your custom monitoring. Ideas: emails, healthcheck API calls, etc.
          } catch {
            case _: Throwable => // tried our best.
          }
      }
  }
 
  // Just a wrapper around Future, so we can match on SafeFuture explicitly.
  def onComplete[U](func: (Try[T]) => U)(implicit executor: ExecutionContext): Unit = future.onComplete(func)
  def isCompleted: Boolean = future.isCompleted
  def value: Option[Try[T]] = future.value
  def ready(atMost: Duration)(implicit permit: CanAwait): this.type = { future.ready(atMost); this }
  def result(atMost: Duration)(implicit permit: CanAwait): T = future.result(atMost)
}
object SafeFuture {
  // Replicate Future {} helper 
  def apply[T](func: => T)(implicit executor: ExecutionContext) = new SafeFuture(Future { func })
  def apply[T](name: String)(func: => T)(implicit executor: ExecutionContext) = new SafeFuture(Future { func }, Some(name))
}

TimeoutFuture

TimeoutFuture lets you establish a SLA timeout for a Future. Simply, if that time passes and the future has not resolved, it resolves as a failure with a TimeoutException. Typically, this should be explicitly handled by your application — supplying a default value, returning an error, etc.

For efficiency, we use netty’s HashWheelTimer. This does not give explicit guarantees about exactly when it runs, and instead provides a best-effort timer. So: It’s much lighter than a scheduler, but less accurate.

object TimeoutFuture {
  def apply[T](future: Future[T], onTimeout: => Unit = Unit)(implicit ec: ExecutionContext, after: Duration): Future[T] = {
    val timer = new HashedWheelTimer(10, TimeUnit.MILLISECONDS)
    val promise = Promise[T]()
    val timeout = timer.newTimeout(new TimerTask {
        def run(timeout: Timeout){
          onTimeout
          promise.failure(new TimeoutException(s"Future timed out after ${after.toMillis}ms"))
        }
      }, after.toNanos, TimeUnit.NANOSECONDS)
    // does not cancel future, only resolves result in approx duration. Your future may still be running!
    Future.firstCompletedOf(Seq(future, promise.future)).tap(_.onComplete { case result => timeout.cancel() })
  }
}

CancelableFuture

CancelableFuture creates a future that can be cancelled, from a blocking code block. This is not usually for SLA timeout guarantees like above. Rather, it’s for when you have a complex long-running blocking bit of code that you want to be able to kill. So, this returns a method that, once called, will harshly interrupt the thread and stop the code. It’s nasty, be careful with it. Example non-deterministic usage:

val (fut, cancel) = CancellableFuture(Thread.sleep((Math.random*2000).toInt tap println))
Thread.sleep(1000)
val wasCancelled = cancel()
println("wasCancelled: " + wasCancelled)
fut.onFailure { case ex: Throwable => println("failed: " + ex.getClass) }
fut.onSuccess { case i => println("success!" + i) }
object CancelableFuture {
  def apply[T](fun: => T)(implicit ex: ExecutionContext): (Future[T], () => Boolean) = {
    val promise = Promise[T]()
    val future = promise.future
    val threadRef = new AtomicReference[Thread](null)
    promise tryCompleteWith SafeFuture { // If you want, swap with normal `Future`
      val t = Thread.currentThread
      threadRef.synchronized { threadRef.set(t) }
      try fun finally { threadRef.synchronized(threadRef.set(null)) }
    }
 
    (future, () => {
      threadRef.synchronized { Option(threadRef getAndSet null) foreach { _.interrupt() } }
      promise.tryFailure(new CancellationException)
    })
  }
}

All of this code together (with necessary imports) is on Github. Have any other useful utility classes for working with Akka and Futures? Let us know!


Viewing all articles
Browse latest Browse all 15

Trending Articles