All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.gfccollective.util.Retry.scala Maven / Gradle / Ivy

The newest version!
package org.gfccollective.util

import java.util.concurrent.locks.LockSupport

import scala.annotation.tailrec
import scala.concurrent.duration._

/**
 * Helper to retry potentially failing functions
 *
 * @author Gregor Heine
 * @since 10/Apr/2015 16:55
 */
object Retry {
  case class TooManyRetries[I](lastInput: I, wrapped: Option[Exception]) extends RuntimeException

  /**
    * Given an input I, retries a function that returns either a new I or an O, until it returns an O or a maximum number of retries has been reached.
    *
    * @param maxRetryTimes The maximum number of retries, defaults to Long.MaxValue. The function f is called at most maxRetryTimes + 1 times.
    *                      In other words, iff maxRetryTimes == 0, f will be called exactly once, iff maxRetryTimes == 1, it will be called at
    *                      most twice, etc.
    * @param i The initial input
    * @param f The function to (re)try
    * @param log An optional log function to report failed iterations to. By default prints the thrown Exception to the console.
    * @return A successful O if the function succeeded within maxRetryTimes or otherwise throws either TooManyRetries or the last thrown NonFatal Exception.
    *         If the function throws a fatal Error, it is not retried and the error is rethrown.
    */
  @tailrec
  def retryFold[I, O](maxRetryTimes: Long = Long.MaxValue)
                     (i: I)
                     (f: I => Either[I, O])
                     (implicit log: Throwable => Unit = _.printStackTrace): O = {
    (try f(i) catch {
      case e: Exception =>
        if (maxRetryTimes <= 0) {
          throw TooManyRetries(i, Some(e))
        }
        log(e)
        Left(i)
    }) match {
      case Left(i1) if maxRetryTimes <= 0 => throw TooManyRetries(i1, None)
      case Left(i1) => retryFold(maxRetryTimes - 1)(i1)(f)
      case Right(o) => o
    }
  }

  /**
   * Retries a function until it succeeds or a maximum number of retries has been reached.
   *
   * @param maxRetryTimes The maximum number of retries, defaults to Long.MaxValue. The function f is called at most maxRetryTimes + 1 times.
   *                      In other words, iff maxRetryTimes == 0, f will be called exactly once, iff maxRetryTimes == 1, it will be called at
   *                      most twice, etc.
   * @param f The function to (re)try
   * @param log An optional log function to report failed iterations to. By default prints the thrown Exception to the console.
   * @return A successful T if the function succeeded within maxRetryTimes or the last thrown NonFatal Exception otherwise. If the function throws a fatal Error, it is not retried and the error is rethrown.
   */
  def retry[T](maxRetryTimes: Long = Long.MaxValue)
              (f: => T)
              (implicit log: Throwable => Unit = _.printStackTrace): T =
    try {
      retryFold(
        maxRetryTimes
      )(())(_ =>
        Right(f)
      )(log)
    } catch {
      case TooManyRetries(_, Some(e)) => throw e
    }

  /**
    * Given an input I, retries a function that returns either a new I or an O, until it returns an O, a maximum number of retries has been
    * reached, or a retry timeout has been reached. Each retry iteration is being exponentially delayed. The delay grows from a given
    * start value and by a given factor until it reaches a given maximum delay value. If maxRetryTimeout is reached,
    * the last function call is at the point of the timeout. E.g. if the initial delay is 1 second, the retry timeout 10 seconds
    * and all other parameters at their default, the function will be retried after 1, 3 (1+2), 7 (1+2+4) and finally 10 seconds before it fails.
    *
    * @param maxRetryTimes The maximum number of retries, defaults to Long.MaxValue. The function f is called at most maxRetryTimes + 1 times.
    *                      In other words, iff maxRetryTimes == 0, f will be called exactly once, iff maxRetryTimes == 1, it will be called at
    *                      most twice, etc.
    * @param maxRetryTimeout The retry Deadline until which to retry the function, defaults to 1 day from now
    * @param initialDelay The initial delay value, defaults to 1 nanosecond
    * @param maxDelay The maximum delay value, defaults to 1 day
    * @param exponentFactor The factor by which the delay increases between retry iterations
    * @param f The function to (re)try
    * @param log An optional log function to report failed iterations to. By default prints the thrown Exception to the console.
    * @return A successful T if the function succeeded within maxRetryTimes and maxRetryTimeout or the last thrown NonFatal Exception otherwise.
    *         If the function throws a fatal Error, it is not retried and the error is rethrown.
    */
  @tailrec
  def retryFoldWithExponentialDelay[I, O](maxRetryTimes: Long = Long.MaxValue,
                                          maxRetryTimeout: Deadline = 1.day.fromNow,
                                          initialDelay: Duration = 1.millisecond,
                                          maxDelay: FiniteDuration = 1.day,
                                          exponentFactor: Double = 2)
                                         (i: I)
                                         (f: I => Either[I, O])
                                         (implicit log: Throwable => Unit = _.printStackTrace): O = {
    require(exponentFactor >= 1.0)
    val delay = Seq(initialDelay, maxDelay, maxRetryTimeout.timeLeft).min
    (try (f(i)) catch {
      case e: Exception =>
        if (maxRetryTimes <= 0 || maxRetryTimeout.isOverdue) {
          throw TooManyRetries(i, Some(e))
        }
        log(e)
        val delayNs = delay.toNanos
        // Under 10ms we use the more precise (but also more spurious) LockSupport.parkNanos, otherwise the more reliable Thread.sleep
        if (delayNs < 10000000L) {
          LockSupport.parkNanos(delayNs)
        } else {
          try {
            Thread.sleep(delayNs / 1000000L, (delayNs % 1000000).toInt)
          } catch {
            case ie: InterruptedException => /* ignore interrupted exceptions */
          }
        }
        Left(i)
    }) match {
      case Left(i1) if maxRetryTimes <= 0 => throw TooManyRetries(i1, None)
      case Left(i1) => retryFoldWithExponentialDelay(maxRetryTimes - 1, maxRetryTimeout, delay * exponentFactor, maxDelay, exponentFactor)(i1)(f)(log)
      case Right(o) => o
    }
  }

  /**
   * Retries a function until it succeeds, a maximum number of retries has been reached, or a retry timeout
   * has been reached. Each retry iteration is being exponentially delayed. The delay grows from a given
   * start value and by a given factor until it reaches a given maximum delay value. If maxRetryTimeout is reached,
   * the last function call is at the point of the timeout. E.g. if the initial delay is 1 second, the retry timeout 10 seconds
   * and all other parameters at their default, the function will be retried after 1, 3 (1+2), 7 (1+2+4) and finally 10 seconds before it fails.
   *
   * @param maxRetryTimes The maximum number of retries, defaults to Long.MaxValue. The function f is called at most maxRetryTimes + 1 times.
   *                      In other words, iff maxRetryTimes == 0, f will be called exactly once, iff maxRetryTimes == 1, it will be called at
   *                      most twice, etc.
   * @param maxRetryTimeout The retry Deadline until which to retry the function, defaults to 1 day from now
   * @param initialDelay The initial delay value, defaults to 1 nanosecond
   * @param maxDelay The maximum delay value, defaults to 1 day
   * @param exponentFactor The factor by which the delay increases between retry iterations
   * @param f The function to (re)try
   * @param log An optional log function to report failed iterations to. By default prints the thrown Exception to the console.
   * @return A successful T if the function succeeded within maxRetryTimes and maxRetryTimeout or the last thrown NonFatal Exception otherwise.
   *         If the function throws a fatal Error, it is not retried and the error is rethrown.
   */
  def retryWithExponentialDelay[T](maxRetryTimes: Long = Long.MaxValue,
                                   maxRetryTimeout: Deadline = 1.day.fromNow,
                                   initialDelay: Duration = 1.millisecond,
                                   maxDelay: FiniteDuration = 1.day,
                                   exponentFactor: Double = 2)
                                  (f: => T)
                                  (implicit log: Throwable => Unit = _.printStackTrace): T =
    try {
      retryFoldWithExponentialDelay(
        maxRetryTimes, maxRetryTimeout, initialDelay, maxDelay, exponentFactor
      )(())(_ =>
        Right(f)
      )(log)
    } catch {
      case TooManyRetries(_, Some(e)) => throw e
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy