web-dev-qa-db-fra.com

Comment attendre plusieurs Futures

Supposons que j'ai plusieurs avenirs et que je dois attendre jusqu'à ce que soit l'un d'entre eux échoue o tous réussissent.

Par exemple: Soit 3 futurs: _f1_, _f2_, _f3_.

  • Si _f1_ réussit et _f2_ échoue, je n'attends pas _f3_ (et renvoie échec au client).

  • Si _f2_ échoue alors que _f1_ et _f3_ sont toujours en cours d'exécution, je ne les attends pas (et retourne échec)

  • Si _f1_ réussit, puis _f2_ réussit, je continue d'attendre _f3_.

Comment le mettriez-vous en œuvre?

82
Michael

Vous pouvez utiliser un for-compréhension comme suit:

val fut1 = Future{...}
val fut2 = Future{...}
val fut3 = Future{...}

val aggFut = for{
  f1Result <- fut1
  f2Result <- fut2
  f3Result <- fut3
} yield (f1Result, f2Result, f3Result)

Dans cet exemple, les contrats à terme 1, 2 et 3 sont lancés en parallèle. Ensuite, dans la compréhension, nous attendons que les résultats 1, puis 2 et 3 soient disponibles. Si 1 ou 2 échoue, nous n'attendrons plus 3. Si les 3 réussissent, alors le aggFut val tiendra un Tuple avec 3 emplacements, correspondant aux résultats des 3 contrats à terme.

Maintenant, si vous avez besoin du comportement qui vous oblige à ne plus attendre, si d'abord dire fut2 échoue, les choses deviennent un peu plus compliquées. Dans l'exemple ci-dessus, vous devrez attendre que fut1 soit terminé avant de réaliser que fut2 a échoué. Pour résoudre ce problème, vous pouvez essayer quelque chose comme ceci:

  val fut1 = Future{Thread.sleep(3000);1}
  val fut2 = Promise.failed(new RuntimeException("boo")).future
  val fut3 = Future{Thread.sleep(1000);3}

  def processFutures(futures:Map[Int,Future[Int]], values:List[Any], prom:Promise[List[Any]]):Future[List[Any]] = {
    val fut = if (futures.size == 1) futures.head._2
    else Future.firstCompletedOf(futures.values)

    fut onComplete{
      case Success(value) if (futures.size == 1)=> 
        prom.success(value :: values)

      case Success(value) =>
        processFutures(futures - value, value :: values, prom)

      case Failure(ex) => prom.failure(ex)
    }
    prom.future
  }

  val aggFut = processFutures(Map(1 -> fut1, 2 -> fut2, 3 -> fut3), List(), Promise[List[Any]]())
  aggFut onComplete{
    case value => println(value)
  }

Maintenant, cela fonctionne correctement, mais le problème vient du fait de savoir lequel Future à supprimer de Map quand celui-ci a été complété avec succès. Tant que vous avez un moyen de corréler correctement un résultat avec le futur qui a engendré ce résultat, alors quelque chose comme ceci fonctionne. Il ne fait que récursivement supprimer les contrats à terme complétés de la carte, puis appeler Future.firstCompletedOf sur le Futures restant jusqu'à ce qu'il n'en reste plus, en collectant les résultats en cours de route. Ce n'est pas joli, mais si vous avez vraiment besoin du comportement dont vous parlez, ceci ou quelque chose de similaire pourrait fonctionner.

78
cmbaxter

Vous pouvez utiliser une promesse et lui envoyer soit le premier échec, soit le dernier succès agrégé terminé:

def sequenceOrBailOut[A, M[_] <: TraversableOnce[_]](in: M[Future[A]] with TraversableOnce[Future[A]])(implicit cbf: CanBuildFrom[M[Future[A]], A, M[A]], executor: ExecutionContext): Future[M[A]] = {
  val p = Promise[M[A]]()

  // the first Future to fail completes the promise
  in.foreach(_.onFailure{case i => p.tryFailure(i)})

  // if the whole sequence succeeds (i.e. no failures)
  // then the promise is completed with the aggregated success
  Future.sequence(in).foreach(p trySuccess _)

  p.future
}

Ensuite, vous pouvez Await sur ce résultat Future si vous souhaitez bloquer, ou simplement map dans un autre domaine.

La différence avec pour compréhension est que vous obtenez ici l'erreur du premier qui échoue, alors qu'avec pour compréhension, vous obtenez la première erreur dans l'ordre de parcours de la collection en entrée (même si une autre a échoué en premier). Par exemple:

val f1 = Future { Thread.sleep(1000) ; 5 / 0 }
val f2 = Future { 5 }
val f3 = Future { None.get }

Future.sequence(List(f1,f2,f3)).onFailure{case i => println(i)}
// this waits one second, then prints "Java.lang.ArithmeticException: / by zero"
// the first to fail in traversal order

Et:

val f1 = Future { Thread.sleep(1000) ; 5 / 0 }
val f2 = Future { 5 }
val f3 = Future { None.get }

sequenceOrBailOut(List(f1,f2,f3)).onFailure{case i => println(i)}
// this immediately prints "Java.util.NoSuchElementException: None.get"
// the 'actual' first to fail (usually...)
// and it returns early (it does not wait 1 sec)
32
gourlaysama

Voici une solution sans utiliser d'acteurs.

import scala.util._
import scala.concurrent._
import Java.util.concurrent.atomic.AtomicInteger

// Nondeterministic.
// If any failure, return it immediately, else return the final success.
def allSucceed[T](fs: Future[T]*): Future[T] = {
  val remaining = new AtomicInteger(fs.length)

  val p = promise[T]

  fs foreach {
    _ onComplete {
      case s @ Success(_) => {
        if (remaining.decrementAndGet() == 0) {
          // Arbitrarily return the final success
          p tryComplete s
        }
      }
      case f @ Failure(_) => {
        p tryComplete f
      }
    }
  }

  p.future
}
7
FranklinChen

Pour ce faire, j'utiliserais un acteur Akka. À la différence du for-compréhension, il échoue dès qu'un des futurs échoue, c'est donc un peu plus efficace en ce sens.

class ResultCombiner(futs: Future[_]*) extends Actor {

  var origSender: ActorRef = null
  var futsRemaining: Set[Future[_]] = futs.toSet

  override def receive = {
    case () =>
      origSender = sender
      for(f <- futs)
        f.onComplete(result => self ! if(result.isSuccess) f else false)
    case false =>
      origSender ! SomethingFailed
    case f: Future[_] =>
      futsRemaining -= f
      if(futsRemaining.isEmpty) origSender ! EverythingSucceeded
  }

}

sealed trait Result
case object SomethingFailed extends Result
case object EverythingSucceeded extends Result

Créez ensuite l'acteur, envoyez-lui un message (pour qu'il sache où envoyer sa réponse) et attendez une réponse.

val actor = actorSystem.actorOf(Props(new ResultCombiner(f1, f2, f3)))
try {
  val f4: Future[Result] = actor ? ()
  implicit val timeout = new Timeout(30 seconds) // or whatever
  Await.result(f4, timeout.duration).asInstanceOf[Result] match {
    case SomethingFailed => println("Oh noes!")
    case EverythingSucceeded => println("It all worked!")
  }
} finally {
  // Avoid memory leaks: destroy the actor
  actor ! PoisonPill
}
5
Robin Green

Vous pouvez le faire avec un avenir seul. Voici une implémentation. Notez que l'exécution ne sera pas terminée plus tôt! Dans ce cas, vous devez faire quelque chose de plus sophistiqué (et probablement mettre en œuvre l'interruption vous-même). Mais si vous ne voulez tout simplement pas attendre que quelque chose ne fonctionne pas, il est essentiel de continuer à attendre que la première chose soit finie et de vous arrêter lorsque rien ne reste ou que vous frappez une exception:

import scala.annotation.tailrec
import scala.util.{Try, Success, Failure}
import scala.concurrent._
import scala.concurrent.duration.Duration
import ExecutionContext.Implicits.global

@tailrec def awaitSuccess[A](fs: Seq[Future[A]], done: Seq[A] = Seq()): 
Either[Throwable, Seq[A]] = {
  val first = Future.firstCompletedOf(fs)
  Await.ready(first, Duration.Inf).value match {
    case None => awaitSuccess(fs, done)  // Shouldn't happen!
    case Some(Failure(e)) => Left(e)
    case Some(Success(_)) =>
      val (complete, running) = fs.partition(_.isCompleted)
      val answers = complete.flatMap(_.value)
      answers.find(_.isFailure) match {
        case Some(Failure(e)) => Left(e)
        case _ =>
          if (running.length > 0) awaitSuccess(running, answers.map(_.get) ++: done)
          else Right( answers.map(_.get) ++: done )
      }
  }
}

En voici un exemple en action lorsque tout va bien:

scala> awaitSuccess(Seq(Future{ println("Hi!") }, 
  Future{ Thread.sleep(1000); println("Fancy meeting you here!") },
  Future{ Thread.sleep(2000); println("Bye!") }
))
Hi!
Fancy meeting you here!
Bye!
res1: Either[Throwable,Seq[Unit]] = Right(List((), (), ()))

Mais quand quelque chose ne va pas:

scala> awaitSuccess(Seq(Future{ println("Hi!") }, 
  Future{ Thread.sleep(1000); throw new Exception("boo"); () }, 
  Future{ Thread.sleep(2000); println("Bye!") }
))
Hi!
res2: Either[Throwable,Seq[Unit]] = Left(Java.lang.Exception: boo)

scala> Bye!
5
Rex Kerr

On a répondu à cette question, mais je publie ma solution de classe de valeur (les classes de valeur ont été ajoutées à la version 2.10) car il n'y en a pas ici. S'il vous plaît n'hésitez pas à critiquer.

  implicit class Sugar_PimpMyFuture[T](val self: Future[T]) extends AnyVal {
    def concurrently = ConcurrentFuture(self)
  }
  case class ConcurrentFuture[A](future: Future[A]) extends AnyVal {
    def map[B](f: Future[A] => Future[B]) : ConcurrentFuture[B] = ConcurrentFuture(f(future))
    def flatMap[B](f: Future[A] => ConcurrentFuture[B]) : ConcurrentFuture[B] = concurrentFutureFlatMap(this, f) // work around no nested class in value class
  }
  def concurrentFutureFlatMap[A,B](outer: ConcurrentFuture[A], f: Future[A] => ConcurrentFuture[B]) : ConcurrentFuture[B] = {
    val p = Promise[B]()
    val inner = f(outer.future)
    inner.future onFailure { case t => p.tryFailure(t) }
    outer.future onFailure { case t => p.tryFailure(t) }
    inner.future onSuccess { case b => p.trySuccess(b) }
    ConcurrentFuture(p.future)
  }

ConcurrentFuture est un encapsuleur Future sans surcharge qui modifie la carte par défaut Future map/flatMap de do-this-then-that en une combinaison de tous et échec si-n'importe-échec. Usage:

def func1 : Future[Int] = Future { println("f1!");throw new RuntimeException; 1 }
def func2 : Future[String] = Future { Thread.sleep(2000);println("f2!");"f2" }
def func3 : Future[Double] = Future { Thread.sleep(2000);println("f3!");42.0 }

val f : Future[(Int,String,Double)] = {
  for {
    f1 <- func1.concurrently
    f2 <- func2.concurrently
    f3 <- func3.concurrently
  } yield for {
   v1 <- f1
   v2 <- f2
   v3 <- f3
  } yield (v1,v2,v3)
}.future
f.onFailure { case t => println("future failed $t") }

Dans l'exemple ci-dessus, les fonctions f1, f2 et f3 s'exécutent simultanément et, en cas d'échec dans un ordre quelconque, l'avenir du tuple échouera immédiatement.

4
lancegatlin

Vous voudrez peut-être consulter la future API de Twitter. Notamment la méthode Future.collect. Il fait exactement ce que vous voulez: https://Twitter.github.io/scala_school/finagle.html

Le code source Future.scala est disponible ici: https://github.com/Twitter/util/blob/master/util-core/src/main/scala/com/Twitter/util/Future.scala

4
JBakouny

Vous pouvez utiliser ceci:

val l = List(1, 6, 8)

val f = l.map{
  i => future {
    println("future " +i)
    Thread.sleep(i* 1000)
    if (i == 12)
      throw new Exception("6 is not legal.")
    i
  }
}

val f1 = Future.sequence(f)

f1 onSuccess{
  case l => {
    logInfo("onSuccess")
    l.foreach(i => {

      logInfo("h : " + i)

    })
  }
}

f1 onFailure{
  case l => {
    logInfo("onFailure")
  }
2
igreenfield