packages feed

tower-hs-0.1.0.0: src/Tower/Middleware/Retry.hs

{-# LANGUAGE NumericUnderscores #-}

-- |
-- Module      : Tower.Middleware.Retry
-- Description : Retry middleware with configurable backoff
-- License     : MIT
--
-- Retries failed requests with constant or exponential backoff.
--
-- @
-- 'withRetry' ('constantBackoff' 3 1.0)
-- 'withRetry' ('exponentialBackoff' 5 0.5 2.0)
-- @
module Tower.Middleware.Retry
  ( BackoffStrategy(..)
  , constantBackoff
  , exponentialBackoff
  , withRetry
  , computeDelay
  ) where

import Control.Concurrent (threadDelay)
import Data.Time.Clock (NominalDiffTime)

import Tower.Service (Service(..), Middleware)
import Tower.Error (ServiceError(..))

-- | Strategy for computing delay between retries.
data BackoffStrategy
  = ConstantBackoff
      { backoffMaxRetries :: !Int
        -- ^ Maximum number of retries.
      , backoffDelay      :: !NominalDiffTime
        -- ^ Fixed delay between retries.
      }
  | ExponentialBackoff
      { backoffMaxRetries :: !Int
        -- ^ Maximum number of retries.
      , backoffBaseDelay  :: !NominalDiffTime
        -- ^ Initial delay before first retry.
      , backoffMultiplier :: !Double
        -- ^ Multiplier applied to delay after each attempt.
      }
  deriving (Show, Eq)

-- | Constant backoff: same delay between every retry.
--
-- @'constantBackoff' 3 1.0@ — retry up to 3 times, 1 second apart.
constantBackoff :: Int -> NominalDiffTime -> BackoffStrategy
constantBackoff = ConstantBackoff

-- | Exponential backoff: delay grows by multiplier each attempt.
--
-- @'exponentialBackoff' 5 0.5 2.0@ — retry up to 5 times, starting at 500ms, doubling each time.
exponentialBackoff :: Int -> NominalDiffTime -> Double -> BackoffStrategy
exponentialBackoff = ExponentialBackoff

-- | Compute the delay for a given attempt number (0-indexed).
computeDelay :: BackoffStrategy -> Int -> NominalDiffTime
computeDelay (ConstantBackoff _ d) _ = d
computeDelay (ExponentialBackoff _ base mult) attempt =
  base * realToFrac (mult ^^ attempt)

-- | Retry middleware: retries failed requests according to the backoff strategy.
--
-- On failure, waits for the computed delay, then retries. After all retries
-- are exhausted, returns 'RetryExhausted' with the attempt count and last error.
withRetry :: BackoffStrategy -> Middleware req res
withRetry strategy inner = Service $ \req ->
  go req 0
  where
    maxRetries = backoffMaxRetries strategy

    go req attempt = do
      result <- runService inner req
      case result of
        Right resp -> pure (Right resp)
        Left err
          | attempt >= maxRetries ->
              pure (Left (RetryExhausted attempt err))
          | otherwise -> do
              let delaySeconds = computeDelay strategy attempt
                  delayMicros = round (delaySeconds * 1_000_000) :: Int
              threadDelay delayMicros
              go req (attempt + 1)