packages feed

shibuya-core-0.8.0.0: src/Shibuya/Internal/Runner/Finalize.hs

-- | __Internal module.__ Exposed for the test suite and benchmarks only.
-- No PVP guarantees: anything here may change or disappear in any release.
-- Application authors should import "Shibuya" instead.
--
-- Shared resilient finalization for framework-owned ack paths.
--
-- Both single-message and batch processing call adapter-provided
-- 'AckHandle.finalize' through this module. A transient finalizer exception is
-- retried with the fixed schedule below; each retry uses the same already
-- resolved 'AckDecision'.
module Shibuya.Internal.Runner.Finalize
  ( finalizeRetryDelaysMicros,
    finalizeWithRetry,
  )
where

import Effectful (Eff, IOE, liftIO, (:>))
import OpenTelemetry.Trace.Core qualified as OTel
import Shibuya.Core.Ack (AckDecision)
import Shibuya.Core.AckHandle (AckHandle (..))
import Shibuya.Core.Ingested (Ingested (..))
import Shibuya.Telemetry.Effect (Tracing, recordException)
import UnliftIO (SomeException, catchAny)
import UnliftIO.Concurrent (threadDelay)

-- | Retry delays, in microseconds, used after failed finalization attempts.
finalizeRetryDelaysMicros :: [Int]
finalizeRetryDelaysMicros = [10_000, 50_000, 250_000]

-- | Call a message finalizer until it succeeds or the bounded retry budget is
-- exhausted. The resolved decision is never recomputed between attempts.
finalizeWithRetry ::
  (IOE :> es, Tracing :> es) =>
  OTel.Span ->
  Ingested es msg ->
  AckDecision ->
  Eff es (Either SomeException ())
finalizeWithRetry traceSpan ingested decision = go finalizeRetryDelaysMicros
  where
    go delays =
      catchAny
        (Right <$> ingested.ack.finalize decision)
        ( \ex -> do
            recordException traceSpan ex
            case delays of
              [] -> pure (Left ex)
              delay : rest -> do
                liftIO $ threadDelay delay
                go rest
        )