3

Suppose I've got a function foo that performs asynchronous computation and returns a Future:

def foo(x: Int)(implicit ec: ExecutionContext): Future[Int] = ???

Now I'd like to retry this computation n times until the computation succeeds.

def retryFoo(x: Int, n: Int)(implicit ec: ExecutionContext): Future[Int] = ???

I would like also to return all exceptions thrown while retrying. So, I define new exception class ExceptionsList and require retryFoo to return ExceptionsList when all retries fail.

case class ExceptionsList(es: List[Exception]) extends Exception { ... }  

How would you write retryFoo to retry foo and return a Future with either the foo result or ExceptionsList ?

Michael
  • 41,026
  • 70
  • 193
  • 341
  • 2
    Should the list of exceptions be returned only if method eventually fails? Or if, for example, retry limit is 5, and method succeeds on 3rd attempt should it return both result and exceptions from the first two failures? – Krzysztof Atłasik Nov 25 '19 at 13:47
  • Thanks, good question ! I wrote I need the exceptions only when _all_ retries fail. However it makes sense to return the previous exceptions even when the last retry succeeds. – Michael Nov 25 '19 at 13:56

2 Answers2

7

I'd probably do something like this:

final case class ExceptionsList(es: List[Throwable]) extends Throwable
def retry[T](n: Int, expr: => Future[T], exs: List[Throwable] = Nil)(implicit ec: ExecutionContext): Future[T] =
  Future.unit.flatMap(_ => expr).recoverWith {
    case e if n > 0 => retry(n - 1, expr, e :: exs)
    case e => Future.failed(new ExceptionsList(e :: exs))
  }

expr is call-by-name since it itself could throw an exception rather than returning a failed Future. I keep the accumulated exceptions in a list, but I guess that's a taste-thing.

Viktor Klang
  • 26,479
  • 7
  • 51
  • 68
2

You can use Future's transformWith and a little recursion to do the retrying. Using transformWith you can pattern match on the success or failure of your future. In the success case, you just return the result of the Success and you're done since you don't care about intermediate failures. On the failure case you make a recursive call where you decrement the retry count and append the exception to a list in your parameter list to keep track of all your failures. If n is ever zero, you've exhausted your retries. In this case you just create a new instance of ExceptionsList and fail the future.

The below example demonstrates the failure case when all futures throw exceptions. You can call retryFoo(3) and you will get a failed future with three exceptions.

case class ExceptionsList(es: List[Throwable]) extends Exception

def retryFoo(n: Int, exceptions: List[Throwable] = List())(implicit ec: ExecutionContext): Future[Int] = {
  if (n == 0) {
    Future.failed(ExceptionsList(exceptions))
  } else {
    Future {
      throw new Exception("fail!")
    }.transformWith {
      case Success(result) => Future(result)
      case Failure(ex) =>
        println("Retrying!")
        retryFoo(n - 1, exceptions :+ ex)
    }
  }
}
Matt Fowler
  • 2,563
  • 2
  • 15
  • 18