packages feed

kafka-effectful-0.3.1.0: src/Kafka/Effectful/Consumer/Interpreter.hs

-- The 'EffectHandler' type synonym in effectful-core expands to a
-- constraint that GHC's redundant-constraint check flags on the
-- handler's signature, even though the constraint is required for
-- 'interpret' to type-check. Suppress the warning at the file level.
{-# OPTIONS_GHC -Wno-redundant-constraints #-}

module Kafka.Effectful.Consumer.Interpreter
  ( -- * Interpreter
    runKafkaConsumer,
  )
where

import Control.Monad (void)
import Data.Foldable (for_)
import Effectful (Eff, IOE, (:>))
import Effectful qualified
import Effectful.Dispatch.Dynamic (EffectHandler, interpret)
import Effectful.Error.Static (Error, throwError)
import Effectful.Exception (ExitCase (..))
import Effectful.Exception qualified as Exception
import Kafka.Consumer (RdKafkaRespErrT (..))
import Kafka.Consumer qualified as K
import Kafka.Consumer.ConsumerProperties (ConsumerProperties)
import Kafka.Consumer.Subscription (Subscription)
import Kafka.Effectful.Consumer.Classify
  ( PollErrorDisposition (..),
    classifyPollError,
    isBenignCommitError,
  )
import Kafka.Effectful.Consumer.Effect (KafkaConsumer (..))
import Kafka.Types (KafkaError (..))

-- | Run the 'KafkaConsumer' effect.
--
-- Acquires a consumer handle from the given properties and subscription,
-- and releases it when the effect scope ends. Errors are thrown via the
-- 'Error' effect.
runKafkaConsumer ::
  (IOE :> es, Error KafkaError :> es) =>
  ConsumerProperties ->
  Subscription ->
  Eff (KafkaConsumer : es) a ->
  Eff es a
runKafkaConsumer props sub action =
  fst
    <$> Exception.generalBracket
      acquire
      release
      (\consumer -> interpret (handleConsumer consumer) action)
  where
    acquire = do
      result <- Effectful.liftIO $ K.newConsumer props sub
      case result of
        Left err -> throwError err
        Right consumer -> pure consumer

    release consumer = \case
      ExitCaseSuccess _ -> do
        mbErr <- Effectful.liftIO $ K.closeConsumer consumer
        for_ mbErr throwError
      ExitCaseException _ ->
        Effectful.liftIO . void $ K.closeConsumer consumer
      ExitCaseAbort ->
        Effectful.liftIO . void $ K.closeConsumer consumer

handleConsumer ::
  (IOE :> es, Error KafkaError :> es) =>
  K.KafkaConsumer ->
  EffectHandler KafkaConsumer es
handleConsumer consumer _env = \case
  PollMessage timeout -> do
    result <- Effectful.liftIO $ K.pollMessage consumer timeout
    case result of
      Left err -> case classifyPollError err of
        PollTimeout -> pure Nothing
        PollBenign -> pure Nothing
        PollThrow -> throwError err
      Right msg -> pure (Just msg)
  PollMessageEither timeout ->
    Effectful.liftIO $ K.pollMessage consumer timeout
  PollMessageBatch timeout batchSize ->
    Effectful.liftIO $ K.pollMessageBatch consumer timeout batchSize
  CommitOffsetMessage oc cr -> throwOnJustCommit $ K.commitOffsetMessage oc consumer cr
  CommitAllOffsets oc -> throwOnJustCommit $ K.commitAllOffsets oc consumer
  CommitPartitionsOffsets oc tps -> throwOnJustCommit $ K.commitPartitionsOffsets oc consumer tps
  StoreOffsets tps -> throwOnJust $ K.storeOffsets consumer tps
  StoreOffsetMessage cr -> throwOnJust $ K.storeOffsetMessage consumer cr
  Assign tps -> throwOnJust $ K.assign consumer tps
  PausePartitions parts ->
    throwOnKafkaErr (K.pausePartitions consumer parts)
  ResumePartitions parts ->
    throwOnKafkaErr (K.resumePartitions consumer parts)
  SeekPartitions tps timeout -> throwOnJust $ K.seekPartitions consumer tps timeout
  Committed timeout parts -> throwOnLeft $ K.committed consumer timeout parts
  Position parts -> throwOnLeft $ K.position consumer parts
  Assignment -> throwOnLeft $ K.assignment consumer
  Subscription -> throwOnLeft $ K.subscription consumer
  AskConsumerHandle -> pure consumer
  where
    throwOnJust action' = do
      mbErr <- Effectful.liftIO action'
      for_ mbErr throwError

    -- Commits get their own thrower: "nothing to commit" is a success.
    throwOnJustCommit action' = do
      mbErr <- Effectful.liftIO action'
      for_ mbErr $ \err ->
        if isBenignCommitError err then pure () else throwError err

    throwOnLeft action' = do
      result <- Effectful.liftIO action'
      case result of
        Left err -> throwError err
        Right a -> pure a

    throwOnKafkaErr action' = do
      err <- Effectful.liftIO action'
      case err of
        KafkaResponseError RdKafkaRespErrNoError -> pure ()
        _ -> throwError err