packages feed

kafka-effectful-0.2.0.0: src/Kafka/Effectful/Producer/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.Producer.Interpreter (
    -- * Interpreter
    runKafkaProducer,
)
where

import Control.Concurrent.MVar qualified as Concurrent
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 qualified as Exception
import Kafka.Effectful.Producer.Effect (KafkaProducer (..))
import Kafka.Producer qualified as K
import Kafka.Producer.ProducerProperties (ProducerProperties)
import Kafka.Transaction qualified as K
import Kafka.Types (KafkaError)

{- | Run the 'KafkaProducer' effect.

Acquires a producer handle from the given properties and releases it
when the effect scope ends. Errors are thrown via the 'Error' effect.
-}
runKafkaProducer ::
    (IOE :> es, Error KafkaError :> es) =>
    ProducerProperties ->
    Eff (KafkaProducer : es) a ->
    Eff es a
runKafkaProducer props action =
    Exception.bracket
        acquire
        (Effectful.liftIO . K.closeProducer)
        (\producer -> interpret (handleProducer producer) action)
  where
    acquire = do
        result <- Effectful.liftIO $ K.newProducer props
        case result of
            Left err -> throwError err
            Right producer -> pure producer

handleProducer ::
    (IOE :> es, Error KafkaError :> es) =>
    K.KafkaProducer ->
    EffectHandler KafkaProducer es
handleProducer producer _env = \case
    ProduceMessage record -> do
        mbErr <- Effectful.liftIO $ K.produceMessage producer record
        for_ mbErr throwError
    ProduceMessage' record cb -> do
        res <- Effectful.liftIO $ K.produceMessage' producer record cb
        case res of
            Left (K.ImmediateError err) -> throwError err
            Right () -> pure ()
    ProduceMessageBatch records -> Effectful.liftIO $ do
        -- Hackage hw-kafka-client-5.3.0 does not export
        -- 'Kafka.Producer.produceMessageBatch', so we inline the same
        -- definition it ships on master: mapM over the list and keep
        -- only the records that failed to enqueue.
        results <- mapM (\r -> (r,) <$> K.produceMessage producer r) records
        pure [(r, err) | (r, Just err) <- results]
    ProduceMessageSync record -> do
        var <- Effectful.liftIO Concurrent.newEmptyMVar
        res <-
            Effectful.liftIO $
                K.produceMessage' producer record (Concurrent.putMVar var)
        case res of
            Left (K.ImmediateError err) -> throwError err
            Right () -> do
                Effectful.liftIO $ K.flushProducer producer
                report <- Effectful.liftIO $ Concurrent.takeMVar var
                case report of
                    K.DeliverySuccess _ offset -> pure offset
                    K.DeliveryFailure _ err -> throwError err
                    K.NoMessageError err -> throwError err
    FlushProducer ->
        Effectful.liftIO $ K.flushProducer producer
    InitTransactions timeout ->
        throwOnJust $ K.initTransactions producer timeout
    BeginTransaction ->
        throwOnJust $ K.beginTransaction producer
    CommitTransaction timeout ->
        Effectful.liftIO $ K.commitTransaction producer timeout
    AbortTransaction timeout ->
        throwOnJust $ K.abortTransaction producer timeout
    SendOffsetsToTransaction consumer record timeout ->
        Effectful.liftIO $
            K.commitOffsetMessageTransaction producer consumer record timeout
    AskProducerHandle ->
        pure producer
  where
    throwOnJust action' = do
        mbErr <- Effectful.liftIO action'
        for_ mbErr throwError