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?
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.
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)
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
}
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
}
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!
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.
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
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")
}