packages feed

rdkafka-hs-1.1.0.0: src/Kafka/Transaction.hs

-----------------------------------------------------------------------------

-----------------------------------------------------------------------------

-- |
-- Module to work wih Kafkas transactional producers.
module Kafka.Transaction
  ( initTransactions,
    beginTransaction,
    commitTransaction,
    abortTransaction,
    commitOffsetMessageTransaction,
    -- , commitTransactionWithOffsets

    TxError,
    getKafkaError,
    kafkaErrorIsFatal,
    kafkaErrorIsRetriable,
    kafkaErrorTxnRequiresAbort,
  )
where

import Control.Exception (bracket, evaluate)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Foreign.ForeignPtr (finalizeForeignPtr)
import Kafka.Consumer
import Kafka.Consumer.Convert (toNativeTopicPartitionList, topicPartitionFromMessageForCommit)
import Kafka.Internal.RdKafka (RdKafkaErrorTPtr, rdKafkaAbortTransaction, rdKafkaBeginTransaction, rdKafkaCommitTransaction, rdKafkaErrorCode, rdKafkaErrorDestroy, rdKafkaErrorIsFatal, rdKafkaErrorIsRetriable, rdKafkaErrorTxnRequiresAbort, rdKafkaInitTransactions, rdKafkaSendOffsetsToTransaction)
import Kafka.Internal.Setup (getRdKafka)
import Kafka.Producer
import Kafka.Producer.Convert (handleProduceErrT)

-------------------------------------------------------------------------------------
-- Tx API

data TxError = TxError
  { txErrorKafka :: !KafkaError,
    txErrorFatal :: !Bool,
    txErrorRetriable :: !Bool,
    txErrorTxnReqAbort :: !Bool
  }

-- | Initialises Kafka for transactions
initTransactions ::
  (MonadIO m) =>
  KafkaProducer ->
  Timeout ->
  m (Maybe KafkaError)
initTransactions p (Timeout to) =
  liftIO $ bracket (rdKafkaInitTransactions (getRdKafka p) to) rdKafkaErrorDestroy decodeError

-- | Begins a new transaction
beginTransaction ::
  (MonadIO m) =>
  KafkaProducer ->
  m (Maybe KafkaError)
beginTransaction p =
  liftIO $ bracket (rdKafkaBeginTransaction (getRdKafka p)) rdKafkaErrorDestroy decodeError

-- | Commits an existing transaction
-- Pre-condition: there exists an open transaction, created with beginTransaction
commitTransaction ::
  (MonadIO m) =>
  KafkaProducer ->
  Timeout ->
  m (Maybe TxError)
commitTransaction p (Timeout to) = liftIO $ bracket (rdKafkaCommitTransaction (getRdKafka p) to) rdKafkaErrorDestroy toTxError

-- | Aborts an existing transaction
-- Pre-condition: there exists an open transaction, created with beginTransaction
abortTransaction ::
  (MonadIO m) =>
  KafkaProducer ->
  Timeout ->
  m (Maybe KafkaError)
abortTransaction p (Timeout to) =
  liftIO $ bracket (rdKafkaAbortTransaction (getRdKafka p) to) rdKafkaErrorDestroy decodeError

-- | Commits the message's offset in the current transaction
--    Similar to Kafka.Consumer.commitOffsetMessage but within a transactional context
-- Pre-condition: there exists an open transaction, created with beginTransaction
commitOffsetMessageTransaction ::
  (MonadIO m) =>
  KafkaProducer ->
  KafkaConsumer ->
  ConsumerRecord k v ->
  Timeout ->
  m (Maybe TxError)
commitOffsetMessageTransaction p c m (Timeout to) = liftIO $
  bracket (toNativeTopicPartitionList [topicPartitionFromMessageForCommit m]) finalizeForeignPtr $ \tps ->
    bracket (rdKafkaSendOffsetsToTransaction (getRdKafka p) (getRdKafka c) tps to) rdKafkaErrorDestroy toTxError

-- -- | Commit offsets for all currently assigned partitions in the current transaction
-- --    Similar to Kafka.Consumer.commitAllOffsets but within a transactional context
-- -- Pre-condition: there exists an open transaction, created with beginTransaction
-- commitAllOffsetsTransaction :: MonadIO m
--                             => KafkaProducer
--                             -> KafkaConsumer
--                             -> Timeout
--                             -> m (Maybe TxError)
-- commitAllOffsetsTransaction p c (Timeout to) = liftIO $ do
--   -- TODO: this can't be right...
--   tps <- newForeignPtr_ nullPtr
--   rdKafkaSendOffsetsToTransaction (getRdKafka p) (getRdKafka c) tps to >>= toTxError

getKafkaError :: TxError -> KafkaError
getKafkaError = txErrorKafka

kafkaErrorIsFatal :: TxError -> Bool
kafkaErrorIsFatal = txErrorFatal

kafkaErrorIsRetriable :: TxError -> Bool
kafkaErrorIsRetriable = txErrorRetriable

kafkaErrorTxnRequiresAbort :: TxError -> Bool
kafkaErrorTxnRequiresAbort = txErrorTxnReqAbort

----------------------------------------------------------------------------------------------------
-- Implementation detail, used internally
decodeError :: RdKafkaErrorTPtr -> IO (Maybe KafkaError)
decodeError pointer = rdKafkaErrorCode pointer >>= handleProduceErrT

-- The caller scopes ownership around acquisition and all decoding. Strict
-- fields are evaluated before release, so no conversion escapes that scope.
toTxError :: RdKafkaErrorTPtr -> IO (Maybe TxError)
toTxError errPtr = do
  ret <- rdKafkaErrorCode errPtr >>= handleProduceErrT
  case ret of
    Nothing -> pure Nothing
    Just ke -> do
      fatal <- rdKafkaErrorIsFatal errPtr
      retriable <- rdKafkaErrorIsRetriable errPtr
      reqAbort <- rdKafkaErrorTxnRequiresAbort errPtr
      Just
        <$> evaluate
          ( TxError
              { txErrorKafka = ke,
                txErrorFatal = fatal,
                txErrorRetriable = retriable,
                txErrorTxnReqAbort = reqAbort
              }
          )