hw-kafka-client 5.0.0 → 5.3.0
raw patch · 12 files changed
+285/−26 lines, 12 files
Files
- example/ProducerExample.hs +2/−2
- hw-kafka-client.cabal +2/−1
- src/Kafka/Consumer.hs +48/−4
- src/Kafka/Consumer/Callbacks.hs +8/−2
- src/Kafka/Consumer/ConsumerProperties.hs +0/−1
- src/Kafka/Dump.hs +1/−1
- src/Kafka/Internal/RdKafka.chs +69/−1
- src/Kafka/Producer.hs +1/−1
- src/Kafka/Producer/ProducerProperties.hs +7/−1
- src/Kafka/Transaction.hs +132/−0
- src/Kafka/Types.hs +2/−0
- tests-it/Kafka/IntegrationSpec.hs +13/−12
example/ProducerExample.hs view
@@ -50,7 +50,7 @@ putStrLn "Producer is ready, send your messages!" msg1 <- getLine - err1 <- produceMessage prod (mkMessage Nothing (Just $ pack msg1))+ err1 <- produceMessage prod (mkMessage (Just "zero") (Just $ pack msg1)) forM_ err1 print putStrLn "One more time!"@@ -62,7 +62,7 @@ putStrLn "And the last one..." msg3 <- getLine err3 <- produceMessage prod (mkMessage (Just "key3") (Just $ pack msg3))- + err4 <- produceMessage prod ((mkMessage (Just "key4") (Just $ pack msg3)) { prHeaders = headersFromList [("fancy", "header")]}) -- forM_ errs (print . snd)
hw-kafka-client.cabal view
@@ -1,7 +1,7 @@ cabal-version: 2.2 name: hw-kafka-client-version: 5.0.0+version: 5.3.0 synopsis: Kafka bindings for Haskell description: Apache Kafka bindings backed by the librdkafka C library. .@@ -64,6 +64,7 @@ Kafka.Producer Kafka.Producer.ProducerProperties Kafka.Producer.Types+ Kafka.Transaction Kafka.Types other-modules: Kafka.Callbacks Kafka.Consumer.Callbacks
src/Kafka/Consumer.hs view
@@ -55,11 +55,12 @@ , newConsumer , assign, assignment, subscription , pausePartitions, resumePartitions-, committed, position, seek+, committed, position, seek, seekPartitions , pollMessage, pollConsumerEvents , pollMessageBatch , commitOffsetMessage, commitAllOffsets, commitPartitionsOffsets , storeOffsets, storeOffsetMessage+, rewindConsumer , closeConsumer -- ReExport Types , RdKafkaRespErrT (..)@@ -75,16 +76,15 @@ import Data.Bifunctor (bimap, first) import qualified Data.ByteString as BS import Data.IORef (readIORef, writeIORef)-import qualified Data.Map as M+import qualified Data.Map as M hiding (map, foldr) import Data.Maybe (fromMaybe)-import Data.Monoid ((<>)) import Data.Set (Set) import qualified Data.Set as Set import qualified Data.Text as Text import Foreign hiding (void) import Kafka.Consumer.Convert (fromMessagePtr, fromNativeTopicPartitionList'', offsetCommitToBool, offsetToInt64, toMap, toNativeTopicPartitionList, toNativeTopicPartitionList', toNativeTopicPartitionListNoDispose, topicPartitionFromMessageForCommit) import Kafka.Consumer.Types (KafkaConsumer (..))-import Kafka.Internal.RdKafka (RdKafkaRespErrT (..), RdKafkaTopicPartitionListTPtr, RdKafkaTypeT (..), newRdKafkaT, newRdKafkaTopicPartitionListT, newRdKafkaTopicT, rdKafkaAssign, rdKafkaAssignment, rdKafkaCommit, rdKafkaCommitted, rdKafkaConfSetDefaultTopicConf, rdKafkaConsumeBatchQueue, rdKafkaConsumeQueue, rdKafkaConsumerClose, rdKafkaConsumerPoll, rdKafkaOffsetsStore, rdKafkaPausePartitions, rdKafkaPollSetConsumer, rdKafkaPosition, rdKafkaQueueDestroy, rdKafkaQueueNew, rdKafkaResumePartitions, rdKafkaSeek, rdKafkaSetLogLevel, rdKafkaSubscribe, rdKafkaSubscription, rdKafkaTopicConfDup, rdKafkaTopicPartitionListAdd)+import Kafka.Internal.RdKafka (RdKafkaRespErrT (..), RdKafkaTopicPartitionListTPtr, RdKafkaTypeT (..), rdKafkaSeekPartitions, rdKafkaErrorDestroy, rdKafkaErrorCode, newRdKafkaT, newRdKafkaTopicPartitionListT, newRdKafkaTopicT, rdKafkaAssign, rdKafkaAssignment, rdKafkaCommit, rdKafkaCommitted, rdKafkaConfSetDefaultTopicConf, rdKafkaConsumeBatchQueue, rdKafkaConsumeQueue, rdKafkaConsumerClose, rdKafkaConsumerPoll, rdKafkaOffsetsStore, rdKafkaPausePartitions, rdKafkaPollSetConsumer, rdKafkaPosition, rdKafkaQueueDestroy, rdKafkaQueueNew, rdKafkaResumePartitions, rdKafkaSeek, rdKafkaSetLogLevel, rdKafkaSubscribe, rdKafkaSubscription, rdKafkaTopicConfDup, rdKafkaTopicPartitionListAdd) import Kafka.Internal.Setup (CallbackPollStatus (..), Kafka (..), KafkaConf (..), KafkaProps (..), TopicConf (..), TopicProps (..), getKafkaConf, getRdKafka, kafkaConf, topicConf, Callback(..)) import Kafka.Internal.Shared (kafkaErrorToMaybe, maybeToLeft, rdKafkaErrorToEither) @@ -255,6 +255,7 @@ KafkaResponseError <$> rdKafkaResumePartitions k pl -- | Seek a particular offset for each provided 'TopicPartition'+{-# DEPRECATED seek "Use seekPartitions instead" #-} seek :: MonadIO m => KafkaConsumer -> Timeout -> [TopicPartition] -> m (Maybe KafkaError) seek (KafkaConsumer (Kafka k) _) (Timeout timeout) tps = liftIO $ either Just (const Nothing) <$> seekAll@@ -271,6 +272,14 @@ nt <- newRdKafkaTopicT k (Text.unpack tn) Nothing return $ bimap KafkaError (,tpPartition tp, tpOffset tp) (first Text.pack nt) +-- | Seek consumer for partitions in partitions to the per-partition+-- offset in the offset field of partitions.+seekPartitions :: MonadIO m => KafkaConsumer -> [TopicPartition] -> Timeout -> m (Maybe KafkaError)+seekPartitions (KafkaConsumer (Kafka k) _) ps (Timeout timeout) = liftIO $ do+ tps <- toNativeTopicPartitionList ps+ err <- bracket (rdKafkaSeekPartitions k tps timeout) rdKafkaErrorDestroy rdKafkaErrorCode+ pure $ either Just (const Nothing) $ rdKafkaErrorToEither err+ -- | Retrieve committed offsets for topics+partitions. committed :: MonadIO m => KafkaConsumer -> Timeout -> [(TopicName, PartitionId)] -> m (Either KafkaError [TopicPartition]) committed (KafkaConsumer (Kafka k) _) (Timeout timeout) tps = liftIO $ do@@ -323,6 +332,41 @@ readIORef qr >>= mapM_ rdKafkaQueueDestroy res <- kafkaErrorToMaybe . KafkaResponseError <$> rdKafkaConsumerClose k pure (CallbackPollDisabled, res)++-- | Rewind consumer's consume position to the last committed offsets for the current assignment.+-- NOTE: follows https://github.com/edenhill/librdkafka/blob/master/examples/transactions.c#L166+rewindConsumer :: MonadIO m + => KafkaConsumer + -> Timeout+ -> m (Maybe KafkaError)+rewindConsumer c to = liftIO $ do+ ret <- assignment c+ case ret of+ Left err -> pure $ Just err+ Right os -> do+ if M.size os == 0+ -- No current assignment to rewind+ then pure Nothing+ else do+ let tps = foldr (\(t, ps) acc -> map (t,) ps ++ acc) [] $ M.toList os+ ret' <- committed c to tps+ case ret' of+ Left err -> pure $ Just err+ Right ps -> do+ -- Seek to committed offset, or start of partition if no+ -- committed offset is available.+ let ps' = map checkOffsets ps+ seekPartitions c ps' to+ where+ checkOffsets :: TopicPartition -> TopicPartition+ checkOffsets tp + | isUncommitedOffset $ tpOffset tp + = tp { tpOffset = PartitionOffsetBeginning }+ | otherwise = tp ++ isUncommitedOffset :: PartitionOffset -> Bool+ isUncommitedOffset (PartitionOffset _) = False+ isUncommitedOffset _ = True ----------------------------------------------------------------------------- newConsumerConf :: ConsumerProperties -> IO KafkaConf newConsumerConf ConsumerProperties {cpProps = m, cpCallbacks = cbs} = do
src/Kafka/Consumer/Callbacks.hs view
@@ -66,7 +66,10 @@ case e of KafkaResponseError RdKafkaRespErrAssignPartitions -> do f k (RebalanceBeforeAssign assignment)- void $ rdKafkaAssign kptr pls+ protocol <- rdKafkaRebalanceProtocol kptr+ if protocol == "COOPERATIVE" + then void $ rdKafkaIncrementalAssign kptr pls+ else void $ rdKafkaAssign kptr pls mbq <- getRdMsgQueue $ getKafkaConf k case mbq of@@ -84,6 +87,9 @@ KafkaResponseError RdKafkaRespErrRevokePartitions -> do f k (RebalanceBeforeRevoke assignment)- void $ newForeignPtr_ nullPtr >>= rdKafkaAssign kptr+ protocol <- rdKafkaRebalanceProtocol kptr+ if protocol == "COOPERATIVE" + then void $ rdKafkaIncrementalUnassign kptr pls+ else void $ newForeignPtr_ nullPtr >>= rdKafkaAssign kptr f k (RebalanceRevoke assignment) x -> error $ "Rebalance: UNKNOWN response: " <> show x
src/Kafka/Consumer/ConsumerProperties.hs view
@@ -34,7 +34,6 @@ import Data.Text (Text) import qualified Data.Text as Text import Kafka.Consumer.Types (ConsumerGroupId (..))-import Kafka.Internal.Setup (KafkaConf (..), Callback(..)) import Kafka.Types (BrokerAddress (..), ClientId (..), KafkaCompressionCodec (..), KafkaDebug (..), KafkaLogLevel (..), Millis (..), kafkaCompressionCodecToText, kafkaDebugToText) import Kafka.Consumer.Callbacks as X
src/Kafka/Dump.hs view
@@ -1,6 +1,6 @@ ----------------------------------------------------------------------------- -- |--- Module providing various functions to dump information. These may be useful for +-- Module providing various functions to dump information. These may be useful for -- debug/investigation but should probably not be used on production applications. ----------------------------------------------------------------------------- module Kafka.Dump
src/Kafka/Internal/RdKafka.chs view
@@ -443,7 +443,7 @@ rdKafkaConfSetSocketCb :: RdKafkaConfTPtr -> SocketCallback -> IO () rdKafkaConfSetSocketCb conf cb = do cb' <- mkSocketCallback cb- withForeignPtr conf $ \c -> rdKafkaConfSetSocketCb' c cb' >> pure 0+ _ <- withForeignPtr conf $ \c -> rdKafkaConfSetSocketCb' c cb' >> pure (0 :: Int) return () {#fun rd_kafka_conf_set_opaque as ^@@ -611,6 +611,9 @@ {`RdKafkaTPtr', `RdKafkaTopicPartitionListTPtr'} -> `RdKafkaRespErrT' cIntToEnum #} +{#fun rd_kafka_rebalance_protocol as ^+ {`RdKafkaTPtr'} -> `String' #}+ {#fun rd_kafka_assignment as rdKafkaAssignment' {`RdKafkaTPtr', alloca- `Ptr RdKafkaTopicPartitionListT' peekPtr* } -> `RdKafkaRespErrT' cIntToEnum #}@@ -1084,6 +1087,66 @@ fptr <- newForeignPtr_ arrPtr rdKafkaMessageProduceVa' kafkaPtr fptr (cIntConv i) +--- Transactional api++{#fun rd_kafka_init_transactions as rdKafkaInitTransactions+ {`RdKafkaTPtr', `Int'} -> `RdKafkaErrorTPtr' #}++{#fun rd_kafka_begin_transaction as rdKafkaBeginTransaction+ {`RdKafkaTPtr'} -> `RdKafkaErrorTPtr' #}++{#fun rd_kafka_send_offsets_to_transaction as rdKafkaSendOffsetsToTransaction'+ {`RdKafkaTPtr', `RdKafkaTopicPartitionListTPtr', `Ptr ()', `Int' } -> `RdKafkaErrorTPtr' #}++{#fun rd_kafka_commit_transaction as rdKafkaCommitTransaction+ {`RdKafkaTPtr', `Int'} -> `RdKafkaErrorTPtr' #}++{#fun rd_kafka_abort_transaction as rdKafkaAbortTransaction+ {`RdKafkaTPtr', `Int'} -> `RdKafkaErrorTPtr' #}++{#fun rd_kafka_incremental_assign as ^+ {`RdKafkaTPtr', `RdKafkaTopicPartitionListTPtr'} -> `RdKafkaErrorTPtr' #}++{#fun rd_kafka_incremental_unassign as ^+ {`RdKafkaTPtr', `RdKafkaTopicPartitionListTPtr'}+ -> `RdKafkaErrorTPtr' #}++{#fun rd_kafka_consumer_group_metadata as rdKafkaConsumerGroupMetadata+ {`RdKafkaTPtr'} -> `Ptr ()' #}++{#fun rd_kafka_consumer_group_metadata_destroy as rdKafkaConsumerGroupMetadataDestroy+ {`Ptr ()'} -> `()' #}++{#fun rd_kafka_seek_partitions as rdKafkaSeekPartitions+ {`RdKafkaTPtr', `RdKafkaTopicPartitionListTPtr', `Int'} -> `RdKafkaErrorTPtr' #}++rdKafkaSendOffsetsToTransaction :: RdKafkaTPtr -> RdKafkaTPtr -> RdKafkaTopicPartitionListTPtr -> Int -> IO RdKafkaErrorTPtr+rdKafkaSendOffsetsToTransaction p c topicPartition timeOut = do+ metaData <- rdKafkaConsumerGroupMetadata c+ errPtr <- rdKafkaSendOffsetsToTransaction' p topicPartition metaData timeOut+ -- NOTE:need to destroy the metadata otherwise leaking memory+ rdKafkaConsumerGroupMetadataDestroy metaData+ pure errPtr++{#fun rd_kafka_error_is_fatal as rdKafkaErrorIsFatal'+ {`RdKafkaErrorTPtr'} -> `CInt' #}++{#fun rd_kafka_error_is_retriable as rdKafkaErrorIsRetriable'+ {`RdKafkaErrorTPtr'} -> `CInt' #}++{#fun rd_kafka_error_txn_requires_abort as rdKafkaErrorTxnRequiresAbort'+ {`RdKafkaErrorTPtr'} -> `CInt' #}++rdKafkaErrorIsFatal :: RdKafkaErrorTPtr -> IO Bool+rdKafkaErrorIsFatal ptr = boolFromCInt <$> rdKafkaErrorIsFatal' ptr++rdKafkaErrorIsRetriable :: RdKafkaErrorTPtr -> IO Bool+rdKafkaErrorIsRetriable ptr = boolFromCInt <$> rdKafkaErrorIsRetriable' ptr++rdKafkaErrorTxnRequiresAbort :: RdKafkaErrorTPtr -> IO Bool+rdKafkaErrorTxnRequiresAbort ptr = boolFromCInt <$> rdKafkaErrorTxnRequiresAbort' ptr++ -- Marshall / Unmarshall enumToCInt :: Enum a => a -> CInt enumToCInt = fromIntegral . fromEnum@@ -1101,6 +1164,11 @@ boolToCInt True = CInt 1 boolToCInt False = CInt 0 {-# INLINE boolToCInt #-}++boolFromCInt :: CInt -> Bool+boolFromCInt (CInt 0) = False+boolFromCInt (CInt _) = True+{-# INLINE boolFromCInt #-} peekInt64Conv :: (Storable a, Integral a) => Ptr a -> IO Int64 peekInt64Conv = liftM cIntConv . peek
src/Kafka/Producer.hs view
@@ -83,6 +83,7 @@ import Kafka.Producer.Convert (copyMsgFlags, handleProduceErrT, producePartitionCInt) import Kafka.Producer.Types (KafkaProducer (..)) + import Kafka.Producer.ProducerProperties as X import Kafka.Producer.Types as X hiding (KafkaProducer) import Kafka.Types as X@@ -191,7 +192,6 @@ if (l == 0) then pollEvents kp (Just $ Timeout 0) -- to be sure that all the delivery reports are fired else flushProducer kp- ------------------------------------------------------------------------------------ withHeaders :: Headers -> ([RdKafkaVuT] -> IO a) -> IO a
src/Kafka/Producer/ProducerProperties.hs view
@@ -14,6 +14,7 @@ , sendTimeout , statisticsInterval , extraProps+, extraProp , suppressDisconnectLogs , extraTopicProps , debugOptions@@ -27,7 +28,6 @@ import Data.Map (Map) import qualified Data.Map as M import Data.Semigroup as Sem-import Kafka.Internal.Setup (KafkaConf(..), Callback(..)) import Kafka.Types (KafkaDebug(..), Timeout(..), KafkaCompressionCodec(..), KafkaLogLevel(..), BrokerAddress(..), kafkaDebugToText, kafkaCompressionCodecToText, Millis(..)) import Kafka.Producer.Callbacks@@ -102,6 +102,12 @@ -- The full list can be found <https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md here> extraProps :: Map Text Text -> ProducerProperties extraProps m = mempty { ppKafkaProps = m }++-- | Any configuration options that are supported by /librdkafka/.+-- The full list can be found <https://github.com/edenhill/librdkafka/blob/master/CONFIGURATION.md here>+extraProp :: Text -> Text -> ProducerProperties+extraProp k v = mempty { ppKafkaProps = M.singleton k v }+{-# INLINE extraProp #-} -- | Suppresses producer disconnects logs. --
+ src/Kafka/Transaction.hs view
@@ -0,0 +1,132 @@+-----------------------------------------------------------------------------+-- |+-- Module to work wih Kafkas transactional producers.+-- +-----------------------------------------------------------------------------+module Kafka.Transaction+( initTransactions+, beginTransaction+, commitTransaction+, abortTransaction++, commitOffsetMessageTransaction+-- , commitTransactionWithOffsets++, TxError+, getKafkaError+, kafkaErrorIsFatal+, kafkaErrorIsRetriable+, kafkaErrorTxnRequiresAbort+)+where++import Control.Monad.IO.Class (MonadIO (liftIO))+import Kafka.Internal.RdKafka (RdKafkaErrorTPtr, rdKafkaErrorDestroy, rdKafkaErrorIsFatal, rdKafkaErrorIsRetriable, rdKafkaErrorTxnRequiresAbort, rdKafkaErrorCode, rdKafkaInitTransactions, rdKafkaBeginTransaction, rdKafkaCommitTransaction, rdKafkaAbortTransaction, rdKafkaSendOffsetsToTransaction)+import Kafka.Internal.Setup (getRdKafka)+import Kafka.Producer.Convert (handleProduceErrT)+import Kafka.Producer+import Kafka.Consumer.Convert (toNativeTopicPartitionList, topicPartitionFromMessageForCommit)+import Kafka.Consumer++-------------------------------------------------------------------------------------+-- 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 $ rdKafkaInitTransactions (getRdKafka p) to >>= rdKafkaErrorCode >>= handleProduceErrT++-- | Begins a new transaction+beginTransaction :: MonadIO m + => KafkaProducer + -> m (Maybe KafkaError)+beginTransaction p + = liftIO $ rdKafkaBeginTransaction (getRdKafka p) >>= rdKafkaErrorCode >>= handleProduceErrT++-- | 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 $ rdKafkaCommitTransaction (getRdKafka p) to >>= 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 $ do rdKafkaAbortTransaction (getRdKafka p) to >>= rdKafkaErrorCode >>= handleProduceErrT++-- | 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 $ do+ tps <- toNativeTopicPartitionList [topicPartitionFromMessageForCommit m]+ rdKafkaSendOffsetsToTransaction (getRdKafka p) (getRdKafka c) tps to >>= 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+toTxError :: RdKafkaErrorTPtr -> IO (Maybe TxError)+toTxError errPtr = do+ ret <- rdKafkaErrorCode errPtr >>= handleProduceErrT+ case ret of+ Nothing -> do+ -- NOTE: don't forget to free error structure, otherwise we are leaking memory!+ rdKafkaErrorDestroy errPtr+ pure Nothing+ Just ke -> do+ fatal <- rdKafkaErrorIsFatal errPtr+ retriable <- rdKafkaErrorIsRetriable errPtr+ reqAbort <- rdKafkaErrorTxnRequiresAbort errPtr+ -- NOTE: don't forget to free error structure, otherwise we are leaking memory!+ rdKafkaErrorDestroy errPtr+ pure $ Just $ TxError + { txErrorKafka = ke+ , txErrorFatal = fatal+ , txErrorRetriable = retriable+ , txErrorTxnReqAbort = reqAbort+ }
src/Kafka/Types.hs view
@@ -150,6 +150,7 @@ | Gzip | Snappy | Lz4+ | Zstd deriving (Eq, Show, Typeable, Generic) -- | Convert a 'KafkaCompressionCodec' into its /librdkafka/ string equivalent.@@ -161,6 +162,7 @@ Gzip -> "gzip" Snappy -> "snappy" Lz4 -> "lz4" + Zstd -> "zstd" -- | Headers that might be passed along with a record newtype Headers = Headers { unHeaders :: [(BS.ByteString, BS.ByteString)] }
tests-it/Kafka/IntegrationSpec.hs view
@@ -153,21 +153,21 @@ it "should consume empty batch when there are no messages" $ \k -> do res <- pollMessageBatch k (Timeout 1000) (BatchSize 50) length res `shouldBe` 0- + describe "Kafka.Headers.Spec" $ do let testHeaders = headersFromList [("a-header-name", "a-header-value"), ("b-header-name", "b-header-value")] specWithKafka "Headers consumer/producer" consumerProps $ do it "1. sends 2 messages to test topic enriched with headers" $ \(k, prod) -> do void $ receiveMessages k- + res <- sendMessagesWithHeaders (testMessages testTopic) testHeaders prod res `shouldBe` Right () it "2. should receive 2 messages enriched with headers" $ \(k, _) -> do res <- receiveMessages k (length <$> res) `shouldBe` Right 2- - forM_ res $ \rcs -> ++ forM_ res $ \rcs -> forM_ rcs ((`shouldBe` Set.fromList (headersToList testHeaders)) . Set.fromList . headersToList . crHeaders) ----------------------------------------------------------------------------------------------------------------@@ -253,13 +253,14 @@ hasTopic `shouldBe` True it "should return topic metadata" $ \k -> do- res <- topicMetadata k (Timeout 1000) testTopic+ res <- topicMetadata k (Timeout 2000) testTopic res `shouldSatisfy` isRight length . kmBrokers <$> res `shouldBe` Right 1 length . kmTopics <$> res `shouldBe` Right 1 it "should describe all consumer groups" $ \k -> do- res <- allConsumerGroupsInfo k (Timeout 1000)+ res <- allConsumerGroupsInfo k (Timeout 2000)+ res `shouldSatisfy` isRight let groups = either (const []) (fmap giGroup) res let prefixedGroups = filter isTestGroupId groups let resLen = length prefixedGroups@@ -267,15 +268,15 @@ -- fmap giGroup <$> res `shouldBe` Right [testGroupId] it "should describe a given consumer group" $ \k -> do- res <- consumerGroupInfo k (Timeout 1000) testGroupId+ res <- consumerGroupInfo k (Timeout 2000) testGroupId fmap giGroup <$> res `shouldBe` Right [testGroupId] it "should describe non-existent consumer group" $ \k -> do- res <- consumerGroupInfo k (Timeout 1000) "does-not-exist"+ res <- consumerGroupInfo k (Timeout 2000) "does-not-exist" res `shouldBe` Right [] it "should read topic offsets for time" $ \k -> do- res <- topicOffsetsForTime k (Timeout 1000) (Millis 1904057189508) testTopic+ res <- topicOffsetsForTime k (Timeout 2000) (Millis 1904057189508) testTopic res `shouldSatisfy` isRight fmap tpOffset <$> res `shouldBe` Right [PartitionOffsetEnd] @@ -300,13 +301,13 @@ || x == Left (KafkaResponseError RdKafkaRespErrTimedOut)) it "should respect out-of-bound offsets (invalid offset)" $ \k -> do- res <- seek k (Timeout 1000) [TopicPartition testTopic (PartitionId 0) PartitionOffsetInvalid]+ res <- seek k (Timeout 2000) [TopicPartition testTopic (PartitionId 0) PartitionOffsetInvalid] res `shouldBe` Nothing msg <- pollMessage k (Timeout 1000) crOffset <$> msg `shouldBe` Right (Offset 0) it "should respect out-of-bound offsets (huge offset)" $ \k -> do- res <- seek k (Timeout 1000) [TopicPartition testTopic (PartitionId 0) (PartitionOffset 123456)]+ res <- seek k (Timeout 3000) [TopicPartition testTopic (PartitionId 0) (PartitionOffset 123456)] res `shouldBe` Nothing- msg <- pollMessage k (Timeout 1000)+ msg <- pollMessage k (Timeout 2000) crOffset <$> msg `shouldBe` Right (Offset 0)