web-dev-qa-db-fra.com

Scala: Liste [Future] à Future [Liste] sans tenir compte des contrats à terme échoués

Je cherche un moyen de convertir une liste de longueur arbitraire de Futures en Future of List. J'utilise Playframework, donc au final, ce que je veux vraiment, c'est un Future[Result], Mais pour simplifier les choses, disons simplement Future[List[Int]] La façon habituelle de faire ceci serait d'utiliser Future.sequence(...) mais il y a une torsion ... La liste que je vous donne contient généralement environ 10 à 20 futurs, et il n'est pas rare qu'un de ces futurs échoue (ils font des demandes de service Web externes). Au lieu de devoir tous les réessayer en cas d'échec de l'un d'entre eux, j'aimerais pouvoir identifier ceux qui ont réussi et les renvoyer.

Par exemple, ce qui suit ne fonctionne pas

import scala.concurrent._
import scala.concurrent.ExecutionContext.Implicits.global
import scala.util.Success
import scala.util.Failure

val listOfFutures = Future.successful(1) :: Future.failed(new Exception("Failure")) :: 
                    Future.successful(3) :: Nil

val futureOfList = Future.sequence(listOfFutures)

futureOfList onComplete {
  case Success(x) => println("Success!!! " + x)
  case Failure(ex) => println("Failed !!! " + ex)
}

scala> Failed !!! Java.lang.Exception: Failure

Au lieu d’obtenir la seule exception à la règle, j’aimerais pouvoir extraire les points 1 et 3. J'ai essayé d'utiliser Future.fold, Mais apparemment, cela n'appelle que Future.sequence Dans les coulisses.

Merci d'avance pour l'aide!

107
Joe

L'astuce consiste d'abord à s'assurer qu'aucun des avenirs n'a échoué. .recover _ est votre ami ici, vous pouvez le combiner avec map pour convertir tous les Future[T] résultats pour Future[Try[T]]] _ instances, qui sont toutes sûrement des avenirs réussis.

note: vous pouvez aussi utiliser Option ou Either ici, mais Try est le moyen le plus propre si vous voulez spécifiquement intercepter des exceptions

def futureToFutureTry[T](f: Future[T]): Future[Try[T]] =
  f.map(Success(_)).recover(x => Failure(x))

val listOfFutures = ...
val listOfFutureTrys = listOfFutures.map(futureToFutureTry(_))

Ensuite, utilisez Future.sequence _ comme avant, pour vous donner un Future[List[Try[T]]]

val futureListOfTrys = Future.sequence(listOfFutureTrys)

Puis filtrer:

val futureListOfSuccesses = futureListOfTrys.map(_.filter(_.isSuccess))

Vous pouvez même extraire les échecs spécifiques, si vous en avez besoin:

val futureListOfFailures = futureListOfTrys.map(_.filter(_.isFailure))
132
Kevin Wright

J'ai essayé la réponse de Kevin et j'ai rencontré un problème sur ma version de Scala (2.11.5) ... j'ai corrigé cela et écrit quelques tests supplémentaires si quelqu'un est intéressé ... voici ma version>

implicit class FutureCompanionOps(val f: Future.type) extends AnyVal {

    /** Given a list of futures `fs`, returns the future holding the list of Try's of the futures from `fs`.
      * The returned future is completed only once all of the futures in `fs` have been completed.
      */
    def allAsTrys[T](fItems: /* future items */ List[Future[T]]): Future[List[Try[T]]] = {
      val listOfFutureTrys: List[Future[Try[T]]] = fItems.map(futureToFutureTry)
      Future.sequence(listOfFutureTrys)
    }

    def futureToFutureTry[T](f: Future[T]): Future[Try[T]] = {
      f.map(Success(_)) .recover({case x => Failure(x)})
    }

    def allFailedAsTrys[T](fItems: /* future items */ List[Future[T]]): Future[List[Try[T]]] = {
      allAsTrys(fItems).map(_.filter(_.isFailure))
    }

    def allSucceededAsTrys[T](fItems: /* future items */ List[Future[T]]): Future[List[Try[T]]] = {
      allAsTrys(fItems).map(_.filter(_.isSuccess))
    }
}


// Tests... 



  // allAsTrys tests
  //
  test("futureToFutureTry returns Success if no exception") {
    val future =  Future.futureToFutureTry(Future{"mouse"})
    Thread.sleep(0, 100)
    val futureValue = future.value
    assert(futureValue == Some(Success(Success("mouse"))))
  }
  test("futureToFutureTry returns Failure if exception thrown") {
    val future =  Future.futureToFutureTry(Future{throw new IllegalStateException("bad news")})
    Thread.sleep(5)            // need to sleep a LOT longer to get Exception from failure case... interesting.....
    val futureValue = future.value

    assertResult(true) {
      futureValue match {
        case Some(Success(Failure(error: IllegalStateException)))  => true
      }
    }
  }
  test("Future.allAsTrys returns Nil given Nil list as input") {
    val future =  Future.allAsTrys(Nil)
    assert ( Await.result(future, 100 nanosecond).isEmpty )
  }
  test("Future.allAsTrys returns successful item even if preceded by failing item") {
    val future1 =  Future{throw new IllegalStateException("bad news")}
    var future2 = Future{"dog"}

    val futureListOfTrys =  Future.allAsTrys(List(future1,future2))
    val listOfTrys =  Await.result(futureListOfTrys, 10 milli)
    System.out.println("successItem:" + listOfTrys);

    assert(listOfTrys(0).failed.get.getMessage.contains("bad news"))
    assert(listOfTrys(1) == Success("dog"))
  }
  test("Future.allAsTrys returns successful item even if followed by failing item") {
    var future1 = Future{"dog"}
    val future2 =  Future{throw new IllegalStateException("bad news")}

    val futureListOfTrys =  Future.allAsTrys(List(future1,future2))
    val listOfTrys =  Await.result(futureListOfTrys,  10 milli)
    System.out.println("successItem:" + listOfTrys);

    assert(listOfTrys(1).failed.get.getMessage.contains("bad news"))
    assert(listOfTrys(0) == Success("dog"))
  }
  test("Future.allFailedAsTrys returns the failed item and only that item") {
    var future1 = Future{"dog"}
    val future2 =  Future{throw new IllegalStateException("bad news")}

    val futureListOfTrys =  Future.allFailedAsTrys(List(future1,future2))
    val listOfTrys =  Await.result(futureListOfTrys,  10 milli)
    assert(listOfTrys(0).failed.get.getMessage.contains("bad news"))
    assert(listOfTrys.size == 1)
  }
  test("Future.allSucceededAsTrys returns the succeeded item and only that item") {
    var future1 = Future{"dog"}
    val future2 =  Future{throw new IllegalStateException("bad news")}

    val futureListOfTrys =  Future.allSucceededAsTrys(List(future1,future2))
    val listOfTrys =  Await.result(futureListOfTrys,  10 milli)
    assert(listOfTrys(0) == Success("dog"))
    assert(listOfTrys.size == 1)
  }
10
Chris Bedford

Scala 2.12 a une amélioration sur Future.transform qui se prête à une réponse avec moins de codes.

val futures = Seq(Future{1},Future{throw new Exception})

// instead of `map` and `recover`, use `transform`
val seq = Future.sequence(futures.map(_.transform(Success(_)))) 

val successes = seq.map(_.collect{case Success(x)=>x})
successes
//res1: Future[Seq[Int]] = Future(Success(List(1)))

val failures = seq.map(_.collect{case Failure(x)=>x})
failures
//res2: Future[Seq[Throwable]] = Future(Success(List(Java.lang.Exception)))
6
WeiChing Lin

Je viens de tomber sur cette question et ai une autre solution à offrir:

def allSuccessful[A, M[X] <: TraversableOnce[X]](in: M[Future[A]])
                                                (implicit cbf: CanBuildFrom[M[Future[A]], A, M[A]], 
                                                 executor: ExecutionContext): Future[M[A]] = {
    in.foldLeft(Future.successful(cbf(in))) {
      (fr, fa) ⇒ (for (r ← fr; a ← fa) yield r += a) fallbackTo fr
    } map (_.result())
}

L'idée ici est que, dans le pli, vous attendez que l'élément suivant de la liste soit complet (à l'aide de la syntaxe for-compréhension) et si l'élément suivant échoue, vous ne faites que revenir à ce que vous avez déjà.

6
Idan Waisman

Vous pouvez facilement envelopper les résultats futurs avec une option, puis aplatir la liste:

def futureToFutureOption[T](f: Future[T]): Future[Option[T]] =
    f.map(Some(_)).recover {
      case e => None
    }
val listOfFutureOptions = listOfFutures.map(futureToFutureOption(_))

val futureListOfOptions = Future.sequence(listOfFutureOptions)

val futureListOfSuccesses = futureListOfOptions.flatten
1