packages feed

kioku-core-0.1.0.0: src/Kioku/Worker/Failure.hs

-- | Failure classification shared by kioku's background workers.
--
-- Both workers must answer the same question about every failure: is this
-- worth retrying, is it permanently broken, or does it not belong to me? This
-- module owns the half of that taxonomy that concerns kiroku store errors and
-- the embedding worker's retry schedule. The distillation timers' half lives in
-- "Kioku.Distill.Timer.Outcome".
module Kioku.Worker.Failure
  ( isTransientStoreError,
    embeddingRetryDelay,
  )
where

import Kiroku.Store.Error (StoreError (..))
import Shibuya.Core.Ack (RetryDelay (..))
import Shibuya.Core.Types (Attempt (..))

-- | Is this store error worth retrying?
--
-- Transient errors are the ones kiroku's own constructor documentation calls
-- retryable: a pool timeout, a connection torn down mid-operation, and the
-- catch-all connection error. Everything else describes a stable disagreement
-- between the caller and the database — a version conflict, a missing stream, a
-- server error whose SQLSTATE kiroku does not recognise — and would fail
-- identically on every redelivery.
--
-- The match lists every constructor explicitly rather than falling through a
-- wildcard so that @-Wincomplete-patterns@ forces a classification decision if
-- a future kiroku version adds one.
isTransientStoreError :: StoreError -> Bool
isTransientStoreError = \case
  PoolAcquisitionTimeout -> True
  ConnectionLost _ -> True
  ConnectionError _ -> True
  WrongExpectedVersion {} -> False
  EmptyAppendBatch _ -> False
  StreamNotFound _ -> False
  ReservedStreamName _ -> False
  StreamNameTooLong _ _ -> False
  StreamAlreadyExists _ -> False
  DuplicateEvent _ -> False
  EventAlreadyLinked _ _ -> False
  LinkSourceEventMissing _ -> False
  UnexpectedServerError _ _ -> False

-- | Backoff for a redelivered @MemoryRecorded@ event, by zero-based delivery
-- attempt: 5s, 20s, 60s, then 180s.
--
-- The bound lives upstream, not here: the shibuya-kiroku adapter maps 'AckRetry'
-- onto kiroku's per-subscription retry policy, which dead-letters after five
-- total deliveries. At most four of these delays are ever consumed, and an
-- outage outlasting that window is recovered by the worker's startup backfill
-- rather than by retrying forever. 'Nothing' (an adapter that does not track
-- redeliveries) gets the longest delay.
embeddingRetryDelay :: Maybe Attempt -> RetryDelay
embeddingRetryDelay = \case
  Just (Attempt 0) -> RetryDelay 5
  Just (Attempt 1) -> RetryDelay 20
  Just (Attempt 2) -> RetryDelay 60
  _ -> RetryDelay 180