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

diode.util.Retry.scala Maven / Gradle / Ivy

There is a newer version: 1.1.0
Show newest version
package diode.util

import java.util.concurrent.TimeUnit

import diode.ActionResult.Effect

import scala.concurrent.ExecutionContext
import scala.concurrent.duration._

/**
  * Define a policy for retrying
  */
trait RetryPolicy {
  /**
    * Checks if retry should be attempted.
    *
    * @param reason Reason for failure leading to this retry. Used for filtering.
    * @return
    */
  def canRetry(reason: Throwable): Boolean

  /**
    * Retries an effect. Returns `Left` is retry is not possible and `Right[(RetryPolicy, Effect[T])]` if it is.
    *
    * @param reason Reason for failure leading to this retry. Used for filtering.
    * @param effect Effect to be retried.
    * @return
    */
  def retry[T <: AnyRef](reason: Throwable, effect: Effect[T]): Either[Throwable, (RetryPolicy, Effect[T])]

  def retry[T <: AnyRef](pot: Pot[_], effect: Effect[T]): Either[Throwable, (RetryPolicy, Effect[T])] =
    retry(pot.exceptionOption.getOrElse(new IllegalStateException("Pot is not in a failed state")), effect)
}

object Retry {

  /**
    * Default retry policy that never retries.
    */
  case object None extends RetryPolicy {
    override def canRetry(reason: Throwable) = false

    override def retry[T <: AnyRef](reason: Throwable, effect: Effect[T]) =
      Left(reason)
  }

  private def always(t: Throwable) = true

  /**
    * Retries a max of `retriesLeft` times immediately following a failure.
    *
    * @param retriesLeft Number of retries
    * @param filter A filter to check if the cause of failure should prevent retrying.
    */
  case class Immediate(retriesLeft: Int, filter: Throwable => Boolean = always) extends RetryPolicy {
    override def canRetry(reason: Throwable) =
      retriesLeft > 0 && filter(reason)

    override def retry[T <: AnyRef](reason: Throwable, effect: Effect[T]) = {
      if (canRetry(reason))
        Right((Immediate(retriesLeft - 1, filter), effect))
      else
        Left(reason)
    }
  }

  /**
    * Provides an exponential backoff algorithm for retrying.
    *
    * @param retriesLeft Number of retries
    * @param delay Delay after failure before trying again. Grows on each retry to `prevDelay * exp`
    * @param exp Exponential growth factor for delay. Default is 2.0 leading to delay doubling on every retry.
    * @param filter A filter to check if the cause of failure should prevent retrying.
    */
  case class Backoff(
    retriesLeft: Int,
    delay: FiniteDuration,
    exp: Double = 2.0,
    filter: Throwable => Boolean = always
  )(implicit runner: RunAfter, ec: ExecutionContext) extends RetryPolicy {
    override def canRetry(reason: Throwable) =
      retriesLeft > 0 && filter(reason)

    override def retry[T <: AnyRef](reason: Throwable, effect: Effect[T]) = {
      if (canRetry(reason)) {
        // wrap the effect into a delayed effect
        val delayEffect = runner.effectAfter(delay)(effect)
        // calculate next delay time
        val nextDelay = (delay.toUnit(TimeUnit.MILLISECONDS) * exp).millis
        Right((Backoff(retriesLeft - 1, nextDelay, exp, filter), delayEffect))
      } else
        Left(reason)
    }
  }

  def apply(retries: Int) =
    Immediate(retries)

  def apply(retries: Int, delay: FiniteDuration)(implicit runner: RunAfter, ec: ExecutionContext) =
    Backoff(retries, delay)

  def apply(retries: Int, delay: FiniteDuration, exp: Double)(implicit runner: RunAfter, ec: ExecutionContext) =
    Backoff(retries, delay, exp)
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy