Запуск нескольких фьючерсов параллельно, возврат значения по умолчанию в тайм-аут

Мне нужно запускать несколько фьючерсов параллельно, и программа не должна сбой или зависание.

Теперь я жду по фьючерсам один за другим и использую запасное значение, если есть TimeoutException.

val future1 = // start future1
val future2 = // start future2
val future3 = // start future3

// <- at this point all 3 futures are running

// waits for maximum of timeout1 seconds
val res1 = toFallback(future1, timeout1, Map[String, Int]())
// .. timeout2 seconds 
val res2 = toFallback(future2, timeout2, List[Int]())
// ... timeout3 seconds
val res3 = toFallback(future3, timeout3, Map[String, BigInt]()) 

def toFallback[T](f: Future[T], to: Int, default: T) = {
  Try(Await.result(f, to seconds))
    .recover { case to: TimeoutException => default }
}

Как я вижу, максимальное время ожидания этого фрагмента timeout1 + timeout2 + timeout3

Мой вопрос: как я могу ждать на всех этих фьючерсах сразу, поэтому я могу сократить время ожидания до max(timeout1, timeout2, timeout3)?

EDIT: В конце я использовал модификацию @Jatin и @senia:

private def composeWaitingFuture[T](fut: Future[T], 
                                    timeout: Int, default: T) =
  future { Await.result(fut, timeout seconds) } recover {
    case e: Exception => default
  }

а затем он используется следующим образом:

// starts futures immediately and waits for maximum of timeoutX seconds
val res1 = composeWaitingFuture(future1, timeout1, Map[String, Int]())
val res2 = composeWaitingFuture(future2, timeout2, List[Int]())
val res3 = composeWaitingFuture(future3, timeout3, Map[String, BigInt]()) 

// takes the maximum of max(timeout1, timeout2, timeout3) to complete
val combinedFuture =
  for {
    r1 <- res1
    r2 <- res2
    r3 <- res3
  } yield (r1, r2, r3)

а затем я использую combinedFuture, как мне кажется.

Ответ 1

def toFallback[T](f: Future[T], to: Int, default: T) = {
  future{
  try{
        Await.result(f, to seconds)
   }catch{
        case e:TimeoutException => default
  }
 }

Вы можете сделать этот блок асинхронным, и каждый запрос ждет своего максимального времени. Если слишком много потоков, возможно, есть один поток, который продолжает проверять другие фьючерсы с помощью Akka system scheduler. @Senia ответила ниже на этом.

Ответ 2

Вы можете создать future, который возвращает результаты всех трех фьючерсов с помощью flatMap или для понимания:

val combinedFuture =
  for {
    r1 <- future1
    r2 <- future2
    r3 <- future3
  } yield (r1, r2, r3)

val (r1, r2, r3) = Await.result(combinedFuture , Seq(timeout1, timeout2, timeout3).max)

Если вы используете akka, вы можете завершить свое будущее со значением по умолчанию после таймаута:

implicit class FutureHelper[T](f: Future[T]) extends AnyVal{
  import akka.pattern.after
  def orDefault(t: Timeout, default: => T)(implicit system: ActorSystem): Future[T] = {
    val delayed = after(t.duration, system.scheduler)(Future.successful(default))
    Future firstCompletedOf Seq(f, delayed)
  }
}

val combinedFuture =
  for {
    r1 <- future1.orDefault(timeout1, Map())
    r2 <- future2.orDefault(timeout2, List())
    r3 <- future3.orDefault(timeout3, Map())
  } yield (r1, r2, r3)

val (r1, r2, r3) = Await.result(combinedFuture , allowance + Seq(timeout1, timeout2, timeout3).max)

Ответ 3

Я бы избегал использования Await.result, поскольку для этого используется поток только для блокировки. Одним из вариантов реализации тайм-аута для фьючерсов будет следующее:

val timer = new Timer()

def toFallback[T](f: Future[T], timeout: Int, default: T) = {
  val p = Promise[T]()
  f.onComplete(result => p.tryComplete(result))
  timer.schedule(new TimerTask {
    def run() {
      p.tryComplete(Success(default))
    }
  }, timeout)
  p.future
}

Это создает обещание, которое будет завершено либо будущим, либо результатом по умолчанию после указанного таймаута - в зависимости от того, что наступит раньше.

Чтобы запускать запросы одновременно, вы должны сделать так:

val future1 = // start future1
val future2 = // start future2
val future3 = // start future3

val res1 = toFallback(future1, timeout1, Map[String, Int]())
val res2 = toFallback(future2, timeout2, List[Int]())
val res3 = toFallback(future3, timeout3, Map[String, BigInt]())

val resultF = for {
  r1 <- res1
  r2 <- res2
  r3 <- res3
} yield (r1, r2, r3)

val (r1, r2, r3) = Await.result(resultF, Duration.Inf)
println(s"$r1, $r2, $r3")

//or
resultF.onSuccess {
  case (r1, r2, r3) => println(s"$r1, $r2, $r3")
}

Ответ 4

Здесь более длинный (unakka) ответ, который учитывает то, что может быть прецедентом, а именно, если одно из значений "время ожидания" вы хотите использовать значение по умолчанию для этого результата, а также сделать что-то с ним (например, отменить долгосрочный расчет или ввод/вывод или что-то еще).

Излишне говорить, что другая история состоит в том, чтобы свести к минимуму блокировку.

Основная идея - сидеть в цикле, ожидая firstCompletedOf элементов, которые еще не были выполнены. Тайм-аут ready - минимальный оставшийся тайм-аут.

В этом коде используются длительные сроки, а не длительность, но длительность, как "оставшееся время", проста.

import scala.language.postfixOps
import scala.concurrent._
import scala.concurrent.duration._
import ExecutionContext.Implicits._
import scala.reflect._
import scala.util._
import java.lang.System.{ nanoTime => now }

import Test.time

class Test {

  type WorkUnit[A] = (Promise[A], Future[A], Deadline, A)
  type WorkQ[A] = Seq[WorkUnit[A]]

  def await[A: ClassTag](work: Seq[(Future[A], Deadline, A)]): Seq[A] = {
    // check for timeout; if using Duration instead of Deadline, decrement here
    def ticktock(w: WorkUnit[A]): WorkUnit[A] = w match {
      case (p, f, t, v) if !p.isCompleted && t.isOverdue => p trySuccess v ; w
      case _ => w
    }
    def await0(work: WorkQ[A]): WorkQ[A] = {
      val live = work filterNot (_._1.isCompleted)
      val t0 = (live map (_._3)).min
      Console println s"Next deadline in ${t0.timeLeft.toMillis}"
      val f0 = Future firstCompletedOf (live map (_._2))
      Try(Await ready (f0, t0.timeLeft))
      val next = work map (w => ticktock(w))
      if (next exists (!_._1.isCompleted)) {
        await0(next)
      } else {
        next
      }
    }
    val wq = work map (_ match {
      case (f, t, v) =>
        val p = Promise[A]
        p.future onComplete (x => Console println s"Value available: $x: $time")
        f onSuccess {
          case a: A => p trySuccess a  // doesn't match on primitive A
          case x => p trySuccess x.asInstanceOf[A]
        }
        f onFailure { case _ => p trySuccess v }
        (p, f, t, v)
    })
    await0(wq) map (_ match {
      case (p, f, t, v) => p.future.value.get.get
    })
  }
}

object Test {
  val start = now
  def time = s"The time is ${ Duration fromNanos (now - start) toMillis }"

  def main(args: Array[String]): Unit = {
    // #2 times out
    def calc(i: Int) = {
      val t = if (args.nonEmpty && i == 2) 6 else i
      Thread sleep t * 1000L
      Console println s"Calculate $i: $time"
      i
    }
    // futures to be completed by a timeout deadline
    // or else use default and let other work happen
    val work = List(
      (future(calc(1)), 3 seconds fromNow, 10),
      (future(calc(2)), 5 seconds fromNow, 20),
      (future(calc(3)), 7 seconds fromNow, 30)
    )
    Console println new Test().await(work)
  }
}

Пример прогона:

[email protected]:~/tmp$ skalac nextcompleted.scala ; skala nextcompleted.Test 
Next deadline in 2992
Calculate 1: The time is 1009
Value available: Success(1): The time is 1012
Next deadline in 4005
Calculate 2: The time is 2019
Value available: Success(2): The time is 2020
Next deadline in 4999
Calculate 3: The time is 3020
Value available: Success(3): The time is 3020
List(1, 2, 3)
[email protected]:~/tmp$ skala nextcompleted.Test arg
Next deadline in 2992
Calculate 1: The time is 1009
Value available: Success(1): The time is 1012
Next deadline in 4005
Calculate 3: The time is 3020
Value available: Success(3): The time is 3020
Next deadline in 1998
Value available: Success(20): The time is 5020
List(1, 20, 3)

Ответ 5

Почему бы не получить сам Future для выполнения исключения и возврата по умолчанию? Затем вы можете просто Await в каждом будущем по очереди, и вам не нужно беспокоиться об обработке исключений за пределами будущего.

Ответ 6

Возможно, это немного взломанно, но вы можете просто измерить прошедшее время и соответственно изменить таймауты. Предполагая timeout1 <= timeout2 <= timeout3:

def now     = System.currentTimeMillis();
val start   = now;
def remains(timeout: Long): Long
            = math.max(0, timeout + start - now)

def toFallback[T](f: Future[T], to: Int, default: T) = {
  Try(Await.result(f, remains(to) seconds))
    .recover { case to: TimeoutException => default }
}

Таким образом, каждый таймаут основывается на момент, когда был вызван start = now, поэтому общее время работы не более timeout3. Если тайм-ауты не выполняются, они все еще работают, но некоторые задачи могут работать дольше, чем назначенный таймаут.