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

sec.api.retries.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2020 Scala EventStoreDB Client
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package sec
package api

import java.util.concurrent.TimeoutException
import scala.concurrent.duration.{Duration, FiniteDuration}
import scala.util.control.NonFatal
import cats.effect.implicits.*
import cats.effect.*
import cats.syntax.all.*
import org.typelevel.log4cats.Logger
import sec.utilities.*

private[sec] object retries:

//======================================================================================================================

  sealed abstract case class RetryConfig(
    delay: FiniteDuration,
    maxDelay: FiniteDuration,
    backoffFactor: Double,
    maxAttempts: Int,
    timeout: Option[FiniteDuration]
  )

  object RetryConfig:

    def apply(
      delay: FiniteDuration,
      maxDelay: FiniteDuration,
      backoffFactor: Double,
      maxAttempts: Int,
      timeout: Option[FiniteDuration]
    ): RetryConfig =
      new RetryConfig(delay.min(maxDelay), maxDelay, backoffFactor.max(1), math.max(maxAttempts, 1), timeout) {}

    extension (c: RetryConfig)
      def nextDelay(d: FiniteDuration): FiniteDuration =
        (d * c.backoffFactor).min(c.maxDelay) match
          case f: FiniteDuration    => f
          case _: Duration.Infinite => c.maxDelay

//======================================================================================================================

  final case class Timeout(after: FiniteDuration) extends RuntimeException(s"Timed out after ${format(after)}.")

//======================================================================================================================

  def retry[F[_]: Temporal, A](
    action: F[A],
    actionName: String,
    retryConfig: RetryConfig,
    log: Logger[F]
  )(retryOn: Throwable => Boolean): F[A] =
    import retryConfig.*

    def withTimeout(to: FiniteDuration): F[A] =
      action.timeout(to).adaptError { case _: TimeoutException => Timeout(to) }

    val fa          = timeout.fold(action)(withTimeout)
    val logWarn     = retries.logWarn[F](retryConfig, actionName, log)
    val logError    = retries.logError[F](retryConfig, actionName, log)
    val maxAttempts = retryConfig.maxAttempts
    val nextDelay   = retryConfig.nextDelay

    def run(attempts: Int, d: FiniteDuration): F[A] = fa.recoverWith {
      case NonFatal(t) if retryOn(t) =>
        if (attempts <= maxAttempts)
          logWarn(attempts, d, t).whenA(attempts < maxAttempts) *>
            Temporal[F].sleep(d) *> run(attempts + 1, nextDelay(d))
        else
          logError(t) *> t.raiseError[F, A]
    }

    run(1, retryConfig.delay)

//======================================================================================================================

  def logWarn[F[_]](cfg: RetryConfig, action: String, log: Logger[F])(
    attempt: Int,
    delay: FiniteDuration,
    th: Throwable
  ): F[Unit] =
    log.warn(s"$action failed, attempt $attempt of ${cfg.maxAttempts}, retrying in ${format(delay)} - ${th.getMessage}")

  def logError[F[_]](cfg: RetryConfig, action: String, log: Logger[F])(th: Throwable): F[Unit] =
    log.error(s"$action failed after ${cfg.maxAttempts} attempts - ${th.getMessage}")

//======================================================================================================================




© 2015 - 2024 Weber Informatics LLC | Privacy Policy