packages feed

rdkafka-hs-1.1.0.0: src/Kafka/Producer/Callbacks.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE TypeApplications #-}

module Kafka.Producer.Callbacks
  ( deliveryCallback,
    module X,
  )
where

import Control.Concurrent (forkIOWithUnmask)
import Control.Exception (SomeException, bracket, catch, mask, throwIO, try)
import Control.Monad (void)
import Data.Either (fromRight)
import Foreign.C.Error (getErrno)
import Foreign.Ptr (Ptr, nullPtr)
import Foreign.StablePtr (castPtrToStablePtr, deRefStablePtr, freeStablePtr)
import Foreign.Storable (Storable (peek))
import Kafka.Callbacks as X
import Kafka.Consumer.Types (Offset (..))
import Kafka.Internal.RdKafka (RdKafkaMessageT (..), RdKafkaRespErrT (..), rdKafkaConfSetDrMsgCb)
import Kafka.Internal.Setup (Callback (..), KafkaConf (kcfgCallbackOwner), getRdKafkaConf)
import Kafka.Internal.Shared (kafkaRespErr, readHeaders, readKey, readPayload, readTopic)
import Kafka.Producer.Types (DeliveryReport (..), ProducePartition (..), ProducerRecord (..))
import Kafka.Types (KafkaError (..), TopicName (..))

-- | Sets the callback for delivery reports.
--
--   /Note: A callback should not be a long-running process as it blocks
--   librdkafka from continuing on the thread that handles the delivery
--   callbacks. For callbacks to individual messsages see
--   'Kafka.Producer.produceMessage\''./
--
-- Exceptions from a global callback are reported through the runtime's
-- uncaught-exception handler on a separate Haskell thread. They do not unwind
-- through librdkafka or prevent the per-message callback from running. A
-- per-message callback runs unmasked on its own thread and uses that same
-- runtime exception policy.
deliveryCallback :: (DeliveryReport -> IO ()) -> Callback
deliveryCallback callback = Callback $ \kc -> rdKafkaConfSetDrMsgCb (kcfgCallbackOwner kc) (getRdKafkaConf kc) realCb
  where
    realCb :: t -> Ptr RdKafkaMessageT -> IO ()
    realCb _ mptr = containCallbackException $ mask $ \restore ->
      if mptr == nullPtr
        then getErrno >>= (restore . callback . NoMessageError . kafkaRespErr)
        else do
          s <- peek mptr
          let cbPtr = opaque'RdKafkaMessageT s
              deliver messageCallback = do
                prodRec <- mkProdRec mptr
                let rep =
                      if err'RdKafkaMessageT s /= RdKafkaRespErrNoError
                        then mkErrorReport s prodRec
                        else mkSuccessReport s prodRec
                outcome <- try @SomeException (restore (callback rep))
                -- Forking transfers the closure to a Haskell thread before
                -- the StablePtr root is released, even if the global callback
                -- threw. A long per-message callback never blocks native poll.
                mapM_ (\msgCb -> void $ forkIOWithUnmask (\unmask -> unmask (msgCb rep))) messageCallback
                either throwIO pure outcome
          if cbPtr == nullPtr
            then deliver Nothing
            else bracket (pure $ castPtrToStablePtr cbPtr) freeStablePtr $ \stablePtr -> do
              msgCb <- deRefStablePtr @(DeliveryReport -> IO ()) stablePtr
              deliver (Just msgCb)

-- A Haskell exception cannot cross a foreign callback frame. Throwing on a
-- fresh Haskell thread retains the ordinary uncaught-exception reporting
-- policy without retaining the native message or its StablePtr.
containCallbackException :: IO () -> IO ()
containCallbackException action =
  action `catch` \(exception :: SomeException) ->
    void $ forkIOWithUnmask (\unmask -> unmask (throwIO exception))

mkErrorReport :: RdKafkaMessageT -> ProducerRecord -> DeliveryReport
mkErrorReport msg prodRec = DeliveryFailure prodRec (KafkaResponseError (err'RdKafkaMessageT msg))

mkSuccessReport :: RdKafkaMessageT -> ProducerRecord -> DeliveryReport
mkSuccessReport msg prodRec = DeliverySuccess prodRec (Offset $ offset'RdKafkaMessageT msg)

mkProdRec :: Ptr RdKafkaMessageT -> IO ProducerRecord
mkProdRec pmsg = do
  msg <- peek pmsg
  topic <- readTopic msg
  key <- readKey msg
  payload <- readPayload msg
  flip fmap (fromRight mempty <$> readHeaders pmsg) $ \headers ->
    ProducerRecord
      { prTopic = TopicName topic,
        prPartition = SpecifiedPartition (partition'RdKafkaMessageT msg),
        prKey = key,
        prValue = payload,
        prHeaders = headers
      }