kafka-effectful-0.3.1.0: src/Kafka/Effectful/Consumer/Classify.hs
-- | How the consumer interpreters treat the errors librdkafka hands back
-- /in-band/ — that is, as a @Left@ inside a poll result or a @Just@ from a
-- commit — rather than by throwing.
--
-- Both the plain and the traced interpreter route every such error through this
-- module, so the policy is stated once, is unit-testable without a broker, and
-- cannot drift between the two interpreters.
--
-- The distinction that matters is between conditions that mean __this consumer
-- is broken__ and conditions that are just __flow control__. librdkafka reports
-- both the same way, and treating the second kind as failure is how a healthy
-- consumer ends up in a crash loop: the interpreter throws, the bracket closes
-- the consumer, the supervisor restarts it, it re-subscribes, and it meets the
-- same partition-scoped condition again.
--
-- @since 0.4.0.0
module Kafka.Effectful.Consumer.Classify
( -- * Poll errors
PollErrorDisposition (..),
classifyPollError,
-- * Commit errors
isBenignCommitError,
)
where
import Kafka.Consumer (RdKafkaRespErrT (..))
import Kafka.Types (KafkaError (..))
-- | What an interpreter does with an in-band poll error.
--
-- @since 0.4.0.0
data PollErrorDisposition
= -- | @RdKafkaRespErrTimedOut@: the poll interval elapsed with no
-- message. Not an error at all; the poll returns 'Nothing'.
PollTimeout
| -- | A partition-scoped flow-control condition that a healthy consumer
-- is expected to meet during normal operation. The poll returns
-- 'Nothing' and the consumer keeps running.
PollBenign
| -- | Anything else, including every fatal error: rethrown through the
-- @Error@ effect.
PollThrow
deriving stock (Eq, Show)
-- | Classify an in-band poll error.
--
-- Three codes are treated as benign, each because librdkafka uses it to report
-- a normal, partition-scoped fact rather than a failure of the consumer:
--
-- * @RdKafkaRespErrPartitionEof@ — the consumer has caught up with a
-- partition. Delivered on every catch-up when @enable.partition.eof@ is set,
-- so throwing on it kills a consumer precisely when it has succeeded.
--
-- * @RdKafkaRespErrAutoOffsetReset@ — the consumer's position was reset, or a
-- reset was attempted and refused. librdkafka raises this as a consumer error
-- only under @auto.offset.reset=error@ or when a reset itself fails; a
-- successful reset after retention loss merely logs. Either way the condition
-- is scoped to one partition's position, and it is delivered before any new
-- commit can move that position, so throwing on it loops forever.
--
-- * @RdKafkaRespErrUnknownTopicOrPart@ — the topic or partition is not known
-- yet. Normal inside a topic-creation window, and it resolves itself once
-- metadata propagates.
--
-- Everything else throws. That deliberately includes @RdKafkaRespErrFatal@, the
-- generic code librdkafka delivers once a fatal error has been raised on the
-- client — a fenced static group member being the canonical case — after which
-- the consumer is permanently dead and must be closed. It also includes
-- @RdKafkaRespErrSaslAuthenticationFailed@, where retrying in a poll loop never
-- helps and can lock accounts. Both of those are matched by the catch-all
-- rather than by name, so a fatal cause this table has never heard of still
-- throws; that is the intended failure direction.
--
-- This table is the interpreter-side counterpart of @hw-kafka-streamly@'s
-- @Kafka.Streamly.Stream.isFatal@, which classifies the same codes for stream
-- consumers. They should be read together when either changes.
--
-- @since 0.4.0.0
classifyPollError :: KafkaError -> PollErrorDisposition
classifyPollError = \case
KafkaResponseError RdKafkaRespErrTimedOut -> PollTimeout
KafkaResponseError RdKafkaRespErrPartitionEof -> PollBenign
KafkaResponseError RdKafkaRespErrAutoOffsetReset -> PollBenign
KafkaResponseError RdKafkaRespErrUnknownTopicOrPart -> PollBenign
_ -> PollThrow
{-# INLINE classifyPollError #-}
-- | Whether a commit result means "there was nothing to commit" rather than
-- "the commit failed".
--
-- This holds for exactly @RdKafkaRespErrNoOffset@. hw-kafka-client's own
-- offset-commit callback documentation says the code "is not to be considered
-- an error": it is what librdkafka returns when a commit is requested and no
-- offsets have advanced — an idle consumer, or a shutdown commit on a consumer
-- that never received anything.
--
-- @since 0.4.0.0
isBenignCommitError :: KafkaError -> Bool
isBenignCommitError = (==) (KafkaResponseError RdKafkaRespErrNoOffset)
{-# INLINE isBenignCommitError #-}