Scala: составление результатов фьючерсов с обработкой исключений

Я новичок в Future in Scala и пока не нашел решения своей проблемы. Я пытаюсь добиться следующего (общее описание: пытаюсь получить список гостей для списка отелей, запрашивая каждый отель отдельно):

  1. Выполните n вызовов другого API с указанием времени ожидания для каждого вызова.
  2. Объединить все результаты (преобразовать список списков в список, содержащий все элементы)
  3. Если отдельный вызов завершится неудачно, зарегистрируйте ошибку и верните пустой список (по сути, в этом случае лучше, если я получу частичные результаты, а не результаты вообще)
  4. В идеале, если произошел сбой отдельного вызова, повторите x раз после ожидания в течение определенного периода времени и, в конце концов, потерпите неудачу и обработайте ошибку, как если бы не было повторной попытки.

Вот мой код HotelReservation представляет внешний API, который я бы назвал.

import com.typesafe.scalalogging.slf4j.Logging

import scala.concurrent._, ExecutionContext.Implicits.global
import scala.util.{Failure, Success}

case class Guest(name: String, country: String)

trait HotelReservation extends Logging {

  def getGuests(id: Int): Future[List[Guest]] = Future {
    logger.debug(s"getting guests for id $id")
    id match {
      case 1 => List(new Guest("John", "canada"))
      case 2 => List(new Guest("Harry", "uk"), new Guest("Peter", "canada"))
      case 3 => {
        Thread.sleep(4000)
        List(new Guest("Harry", "austriala"))
      }
      case _ => throw new IllegalArgumentException("unknown hotel id")
    }
  }
}

object HotelReservationImpl extends HotelReservation

HotelSystem делает звонки.

import com.typesafe.scalalogging.slf4j.Logging

import scala.util.control.NonFatal
import scala.util.{Failure, Success}
import scala.concurrent._, duration._, ExecutionContext.Implicits.global

class HotelSystem(hres: HotelReservation) {

  def pollGuests(hotelIds: List[Int]): Future[List[Guest]] = {

    Future.sequence(
  hotelIds.map { id => future {
    try {
      Await.result(hres.getGuests(id), 3 seconds)
    } catch {
      case _: Exception =>
        Console.println(s"failed for id $id")
        List.empty[Guest]
    }

  }
  }
).map(_.fold(List())(_ ++ _)) /*recover { case NonFatal(e) =>
  Console.println(s"failed:", e)
  List.empty[Guest]
}*/
  }
}

И тест.

object HotelSystemTest extends App {

  Console.println("*** hotel test start ***")

  val hres = HotelReservationImpl

  val hotel = new HotelSystem(hres)
  val result = hotel.pollGuests(List(1, 2, 3, 6))

  result onSuccess {
    case r => Console.println(s"success: $r")
  }

  val timeout = 5000
  Console.println(s"waiting for $timeout ms")
  Thread.sleep(timeout)
  Console.println("*** test end ***")
}

1 и 2 работают. Так же как и 3, но я думаю, что где-то читал на SO, что попытка отловить вызов будущего не является хорошей идеей, и лучше использовать recovery. Тем не менее, в этом случае, если я использую recovery, если произошел отдельный сбой, весь вызов завершится неудачно и вернет пустой список. Любые идеи о том, как улучшить это?

1 ответ

Решение

На самом деле есть две вещи, которые вы могли бы сделать по-другому: оставьте пробную версию и не используйте Await с Futures.

Вот лучший способ реализовать pollGuests:

Future.sequence(
   hotelIds.map { hotelId =>
      hres.getGuests(hotelId).recover {
         case e: Exception => List.empty[Guest]
      }
   }
).map(_.flatten)

Первый момент здесь заключается в том, что вам не нужно использовать фьючерсы внутри pollGuests() так как getGuests() уже дает вам будущее. Вы просто создаете новое будущее с recover() так что возможный сбой уже обработан при возврате будущего

Второй момент заключается в том, что вы не должны использовать Await. Он блокирует ваш код до тех пор, пока не будет готово будущее, что может, например, заморозить весь поток пользовательского интерфейса. Я предполагаю, что вы использовали Await, чтобы иметь возможность использовать try-catch, но благодаря recover() тебе это больше не нужно.

Другие вопросы по тегам