Scala Future和TimeoutException:如何知道根本原因?

时间:2016-12-06 15:57:01

标签: scala asynchronous future

假设我有以下代码:

val futureInt1 = getIntAsync1();
val futureInt2 = getIntAsync2();

val futureSum = for {
  int1 <- futureInt1
  int2 <- futureInt2
} yield (int1 + int2) 

val sum = Await.result(futureSum, 60 seconds)

现在假设getIntAsync1getIntAsync2中的一个花了很长时间,导致Await.result抛出异常:

Caused by: java.util.concurrent.TimeoutException: Futures timed out after [60 seconds]

我应该如何知道getIntAsync1getIntAsync2中的哪一个仍处于待定状态并实际导致超时?

请注意,我在这里将2个期货与zip合并,这是一个简单的问题示例,但在我的应用程序中我有不同级别的这种代码(即getIntAsync1本身可以使用Future.zipFuture.sequence,map / flatMap / applicative)

不知怎的,当我的主线程发生超时时,能够记录挂起的并发操作堆栈跟踪,以便我可以知道系统上的瓶子在哪里。

我有一个现有的遗留API后端,但它还没有完全被动,并且很快就会被激活。我试图通过使用并发来增加响应时间。但是,自从使用这种代码后,理解为什么某些东西需要花费很多时间在我的应用程序中变得更加痛苦。我将非常感谢您提供的任何提示,以帮助我调试此类问题。

6 个答案:

答案 0 :(得分:1)

建议的解决方案将每个未来从for block包装到TimelyFuture中,这需要超时和名称。在内部,它使用Await来检测个别超时。 请记住,这种使用期货的方式不适用于生产代码,因为它使用阻止。仅用于诊断才能找出哪些期货需要时间来完成。

package xxx

import java.util.concurrent.TimeoutException

import scala.concurrent.{Future, _}
import scala.concurrent.duration.Duration
import scala.util._
import scala.concurrent.duration._

class TimelyFuture[T](f: Future[T], name: String, duration: Duration) extends Future[T] {

  override def onComplete[U](ff: (Try[T]) => U)(implicit executor: ExecutionContext): Unit = f.onComplete(x => ff(x))

  override def isCompleted: Boolean = f.isCompleted

  override def value: Option[Try[T]] = f.value

  @scala.throws[InterruptedException](classOf[InterruptedException])
  @scala.throws[TimeoutException](classOf[TimeoutException])
  override def ready(atMost: Duration)(implicit permit: CanAwait): TimelyFuture.this.type = {
    Try(f.ready(atMost)(permit)) match {
      case Success(v) => this
      case Failure(e) => this
    }
  }

  @scala.throws[Exception](classOf[Exception])
  override def result(atMost: Duration)(implicit permit: CanAwait): T = {
    f.result(atMost)
  }

  override def transform[S](ff: (Try[T]) => Try[S])(implicit executor: ExecutionContext): Future[S] = {
    val p = Promise[S]()
    Try(Await.result(f, duration)) match {
      case s@Success(_) => ff(s) match {
        case Success(v) => p.success(v)
        case Failure(e) => p.failure(e)
      }
      case Failure(e) => e match {
        case e: TimeoutException => p.failure(new RuntimeException(s"future ${name} has timed out after ${duration}"))
        case _ => p.failure(e)
      }
    }
    p.future
  }

  override def transformWith[S](ff: (Try[T]) => Future[S])(implicit executor: ExecutionContext): Future[S] = {
    val p = Promise[S]()
    Try(Await.result(f, duration)) match {
      case s@Success(_) => ff(s).onComplete({
        case Success(v) => p.success(v)
        case Failure(e) => p.failure(e)
      })
      case Failure(e) => e match {
        case e: TimeoutException => p.failure(new RuntimeException(s"future ${name} has timed out after ${duration}"))
        case _ => p.failure(e)
      }
    }
    p.future
  }
}

object Main {

  import scala.concurrent.ExecutionContext.Implicits.global

  def main(args: Array[String]): Unit = {
    val f = Future {
      Thread.sleep(5);
      1
    }

    val g = Future {
      Thread.sleep(2000);
      2
    }

    val result: Future[(Int, Int)] = for {
      v1 <- new TimelyFuture(f, "f", 10 milliseconds)
      v2 <- new TimelyFuture(g, "g", 10 milliseconds)
    } yield (v1, v2)


    val sum = Await.result(result, 1 seconds) // as expected, this throws exception : "RuntimeException: future g has timed out after 10 milliseconds"
  }
}

答案 1 :(得分:1)

关键是要意识到 the Future 在你的例子中没有超时 - 它是你的调用线程,它最多暂停X时间。

所以,如果你想在你的期货中建模时间,你应该在每个分支上使用zipWith并使用Future来压缩,它将在一定的时间内包含一个值。如果您使用Akka,那么您可以将akka.pattern.after与Future.firstCompletedOf一起使用。

现在,即使你这样做,你怎么知道为什么你的任何期货没有及时完成,也许它们依赖于未完成的其他期货。

问题可归结为:您是否尝试对吞吐量进行根本原因分析?然后你应该监控你的ExecutionContext,而不是你的期货。期货只是价值。

答案 2 :(得分:1)

如果您只是在寻找个人未来需要很长时间(或与他人合并)的信息指标,那么最好的办法是在创建期货时使用包装来记录指标:

    object InstrumentedFuture {
        def now() = System.currentTimeMillis()
        def apply[T](name: String)(code: => T): Future[T] = {
           val start = now()
           val f = Future { 
            code 
           }
           f.onComplete {
              case _ => println(s"Future ${name} took ${now() - start} ms") 
           }
           f
        }
    }


    val future1 = InstrumentedFuture("Calculator") { /*...code...*/ }
    val future2 = InstrumentedFuture("Differentiator") { /*...code...*/ }

答案 3 :(得分:0)

您可以通过调用isComplete方法

来检查未来是否已完成
if (futureInt1.isComplete) { /*futureInt2 must be the culprit */ } 
if (futureInt2.isComplete) { /*futureInt1 must be the culprit */ } 

答案 4 :(得分:0)

作为第一种方法,我建议将你的未来[Int]提升为未来[Try [Int]]。这样的事情:

object impl {

  def checkException[T](in: Future[T]): Future[Try[T]] = 
    in.map(Success(_)).recover {
      case e: Throwable => {   
        Failure(new Exception("Error in future: " + in))
      }
    }

  implicit class FutureCheck(s: Future[Int]) {
   def check = checkException(s)
  }
}

然后是一个结合结果的小函数,类似于:

object test {

  import impl._

  val futureInt1 = Future{ 1 }
  val futureInt2 = Future{ 2 }

  def combine(a: Try[Int], b: Try[Int])(f: (Int, Int) => (Int)) : Try[Int] = {
    if(a.isSuccess && b.isSuccess) {
      Success(f(a.get, b.get))
    }
   else 
     Failure(new Exception("Error adding results"))
  }

  val futureSum = for {
    int1 <- futureInt1.check
    int2 <- futureInt2.check
  } yield combine(int1, int2)(_ + _)
}

在futureSum中,您将获得一个带有整数的Try [Int]或一个与可能的错误相对应的异常的失败。

也许这可能有用

答案 5 :(得分:0)

val futureInt1 = getIntAsync1();
val futureInt2 = getIntAsync2();

val futureSum = for {
  int1 <- futureInt1
  int2 <- futureInt2
} yield (int1 + int2) 

Try(Await.result(futureSum, 60 seconds)) match {
   case Success(sum) => println(sum)
   case Failure(e)   => println("we got timeout. the unfinished futures are: " + List(futureInt1, futureInt2).filter(!_.isCompleted)
}