Kilmer Luiz Aleluia
Kilmer Luiz Aleluia

Reputation: 335

How to repeat Await multiple times?

I'm new to Scala. I want to retry code1 only if it timeout. If another error occurs, I'll handle it in another way. If timeout, I want to try again, for example, 3 times. What is the proper way to doing this?

I'm not sure if using something like code2 is the best way of doing this on Scala. Maybe there are already something better. Is there some cleaner way for breaking? Should I use Await.ready instead of result?

Im also not sure if I should use Await.ready or Await.result.

code1:

Await.result(myMethod(), Duration(120, "seconds"))

code2:

breakable {
  for(_ <- 0 to 3) {
    Try(Await.result(myMethod(), Duration(120, "seconds"))) match {
      case Success(value) => {
        // do something
        break()
      }
       case Failure(_) => // how can I now if it is timeout?
    }
  }
}

Upvotes: 1

Views: 551

Answers (3)

Chema
Chema

Reputation: 2838

You could try to apply the following approach for repeating evaluation.

The following snippet defines a generic retry method: this method takes in an argument, evaluates it within a try - catch block, and re-executes it on failure with a maximum number of attempts.

Making retry take a by-name parameter is what allows it to repeat evaluation of the myMethod block where necessary.

object Test {

  def retry[T](max: Int)(f: => T): T = {
    var tries = 0
    var result: Option[T] = None
    while (result == None) {
      try {
        result = Some(f)
      }
      catch {
        case e: Throwable => tries += 1
        if (tries > max) throw e
        else {
          println(s"failed, retry #$tries")
        }
      }
    }
    result.get
  }

  def myMethod(n: Int): Int = {
    val num = scala.util.Random.nextInt(10)
    if(n < num) throw new Exception
    else num
  }

  def main(args: Array[String]): Unit = {
    println(retry(max = 5){myMethod(3)})
/*
failed, retry #1
failed, retry #2
3
*/
    println(retry(max = 5){myMethod(-1)})
/*
failed, retry #1
failed, retry #2
failed, retry #3
failed, retry #4
failed, retry #5
Exception in thread "main" java.lang.Exception
*/
  }
}

NOTE myMethod function could take a new parameter timeout to introduce some time limit for each function call.

  def myMethod(n: Int, timeout: Long): Int = {
    var num = scala.util.Random.nextInt(10)

    val current = System.currentTimeMillis()
    var running = System.currentTimeMillis()
    while (running - current <= timeout && n < num) {
      num = scala.util.Random.nextInt(10)
      running = System.currentTimeMillis()
    }
    println(s"time goes on ${running - current}")
    if(n < num) throw new Exception
    else num
  }

// retry function doesn't change
println(retry(5){myMethod(5,5000/*five seconds*/)})
/*
time goes on 5001
failed, retry #1
time goes on 5001
1
*/
println(retry(5){myMethod(-1,5000/*five seconds*/)})
/*
time goes on 5001
failed, retry #1
time goes on 5001
failed, retry #2
time goes on 5001
failed, retry #3
time goes on 5001
failed, retry #4
time goes on 5001
failed, retry #5
time goes on 5001
Exception in thread "main" java.lang.Exception
*/

Upvotes: 1

jwvh
jwvh

Reputation: 51271

Waiting on a Future makes the whole Future-thing rather pointless. That being said, there are 3 situations that should be handled:

  1. all attempts time out
  2. a non-time-out failure
  3. a Future completes successfully
import scala.concurrent.duration._
import scala.concurrent.{Future, Await, TimeoutException}
import scala.util.{Try, Success, Failure}

//            v--max number of attempts
LazyList.fill(4)(Try(Await.result(myMethod(), 120.seconds)))
  .dropWhile{case Failure(_:TimeoutException) => true
             case _ => false
  }.headOption.fold{
    //every attempt timed out
  }{
    case Success(value) => //do something
    case Failure(ex:Throwable) => //ex is non-timeout exception
  }

Note: LazyList is a Scala 2.13.x collection. Every element in the list is evaluated only if/when it's needed, so if the 1st one succeeds then none of the other futures will be launched.

Upvotes: 3

Chris C
Chris C

Reputation: 229

You could use recursion to implement a simple retry mechanism. Matching on the TimeoutException failure will allow you to deal specifically with the timeout case.

Here is an example below, with the assumption that myMethod returns a Future[String]:

def myMethod(): Future[String] = ???

val maxAttempts = 3

def retryOnTimeout(attempt: Int = 0): String = {
  Try(Await.result(myMethod(), Duration(120, "seconds"))) match {
    case Success(result) => result
    case Failure(_: TimeoutException) if attempt < maxAttempts => retryOnTimeout(attempt  + 1)
    case Failure(err) => throw err
  }
}

retryOnTimeout could be made more generic to allow it to be used in more than one place, with the function being passed in as a parameter.

Upvotes: 2

Related Questions