hw-kafka-client 3.1.0 → 3.1.1
raw patch · 11 files changed
+225/−94 lines, 11 filesPVP ok
version bump matches the API change (PVP)
API changes (from Hackage documentation)
Files
- README.md +26/−41
- hw-kafka-client.cabal +3/−1
- src/Kafka/Callbacks.hs +33/−3
- src/Kafka/Consumer.hs +60/−6
- src/Kafka/Consumer/Subscription.hs +15/−0
- src/Kafka/Consumer/Types.hs +29/−11
- src/Kafka/Internal/RdKafka.chs +18/−12
- src/Kafka/Producer.hs +1/−1
- src/Kafka/Producer/Callbacks.hs +4/−3
- src/Kafka/Types.hs +24/−3
- tests-it/Kafka/IntegrationSpec.hs +12/−13
README.md view
@@ -1,33 +1,34 @@ # hw-kafka-client+ [](https://circleci.com/gh/haskell-works/hw-kafka-client) Kafka bindings for Haskell backed by the [librdkafka C module](https://github.com/edenhill/librdkafka). -## Credits-This project is inspired by [Haskakafka](https://github.com/cosbynator/haskakafka)-which unfortunately doesn't seem to be actively maintained.- ## Ecosystem-HaskellWorks Kafka ecosystem is described here: https://github.com/haskell-works/hw-kafka -# Consumer+HaskellWorks Kafka ecosystem is described here: <https://github.com/haskell-works/hw-kafka>++## Consumer+ High level consumers are supported by `librdkafka` starting from version 0.9. High-level consumers provide an abstraction for consuming messages from multiple partitions and topics. They are also address scalability (up to a number of partitions) by providing automatic rebalancing functionality. When a new consumer joins a consumer group the set of consumers attempt to "rebalance" the load to assign partitions to each consumer. -### Example:+### Consumer example +See [Running integration tests locally](#running-integration-tests-locally) to learn how to configure a local environment.+ ```bash-$ stack build --flag hw-kafka-client:examples+cabal build --flag examples ``` or ```bash-$ stack build --exec kafka-client-example --flag hw-kafka-client:examples+cabal run kafka-client-example --flag examples ``` A working consumer example can be found here: [ConsumerExample.hs](example/ConsumerExample.hs)</br>@@ -75,7 +76,7 @@ return $ Right () ``` -# Producer+## Producer `kafka-client` producer supports sending messages to multiple topics. Target topic name is a part of each message that is to be sent by `produceMessage`.@@ -117,9 +118,9 @@ In the example above when the producer cannot deliver the message to Kafka, the error will be printed (and the message will be dropped). -### Example+### Producer example -```Haskell+```haskell {-# LANGUAGE OverloadedStrings #-} import Control.Exception (bracket) import Control.Monad (forM_)@@ -166,6 +167,7 @@ ``` ### Synchronous sending of messages+ Because of the asynchronous nature of librdkafka, there is no API to provide synchronous production of messages. It is, however, possible to combine the delivery reports feature with that of callbacks. This can be done using the@@ -215,44 +217,27 @@ librdkafka times out). You should make sure that your configuration reflects the behavior you want out of this functionality. -# Installation+## Running integration tests locally -## Installing librdkafka+[shell.nix](./shell.nix) can be used to provide a working environment that is enough to build and test `hw-kafka-client`. -Although `librdkafka` is available on many platforms, most of-the distribution packages are too old to support `kafka-client`.-As such, we suggest you install from the source:+To be able to run tests locally, `$KAFKA_TEST_BROKER` environment variable is expected to be set (use [shell.nix](./shell.nix) or export manually). -```bash- git clone https://github.com/edenhill/librdkafka- cd librdkafka- ./configure- make && make install-```+`$KAFKA_TEST_BROKER` should contain an IP address of an accessible Kafka broker that will be used to run integration tests against. -Sometimes it is helpful to specify openssl includes explicitly:+With [Docker Compose](./docker-compose.yml) this variable is used to configure Kafka broker to listen on this address: ```- LDFLAGS=-L/usr/local/opt/openssl/lib CPPFLAGS=-I/usr/local/opt/openssl/include ./configure+$ docker-compose up ``` -If you are using Stack with Nix, don't forget to declare `rdkafka` as extra package:+After that, tests can be run as usual: -```yaml-# stack.yaml-nix:- enable: true- packages:- - rdkafka ```--## Installing Kafka--The full Kafka guide is at http://kafka.apache.org/documentation.html#quickstart+$ cabal test --test-show-details=direct+``` -Alternatively `docker-compose` can be used to run Kafka locally inside a Docker container.-To run Kafka inside Docker:+## Credits -```bash-$ docker-compose up-```+This project is inspired by [Haskakafka](https://github.com/cosbynator/haskakafka)+which unfortunately doesn't seem to be actively maintained.
hw-kafka-client.cabal view
@@ -1,7 +1,7 @@ cabal-version: 2.2 name: hw-kafka-client-version: 3.1.0+version: 3.1.1 synopsis: Kafka bindings for Haskell description: Apache Kafka bindings backed by the librdkafka C library. .@@ -87,6 +87,7 @@ test-suite integration-tests type: exitcode-stdio-1.0+ ghc-options: -threaded -rtsopts -with-rtsopts=-N main-is: Spec.hs hs-source-dirs: tests-it ghc-options: -Wall -threaded@@ -108,6 +109,7 @@ test-suite tests type: exitcode-stdio-1.0+ ghc-options: -threaded -rtsopts -with-rtsopts=-N main-is: Spec.hs hs-source-dirs: tests ghc-options: -Wall -threaded
src/Kafka/Callbacks.hs view
@@ -7,18 +7,48 @@ import Kafka.Internal.RdKafka (rdKafkaConfSetErrorCb, rdKafkaConfSetLogCb, rdKafkaConfSetStatsCb) import Kafka.Internal.Setup (HasKafkaConf(..), getRdKafkaConf)-import Kafka.Types (KafkaError(..))+import Kafka.Types (KafkaError(..), KafkaLogLevel(..)) +-- | Add a callback for errors.+--+-- ==== __Examples__+--+-- Basic usage:+--+-- > setCallback (errorCallback myErrorCallback)+-- >+-- > myErrorCallback :: KafkaError -> String -> IO ()+-- > myErrorCallback kafkaError message = print $ show kafkaError <> "|" <> message errorCallback :: HasKafkaConf k => (KafkaError -> String -> IO ()) -> k -> IO () errorCallback callback k = let realCb _ err = callback (KafkaResponseError err) in rdKafkaConfSetErrorCb (getRdKafkaConf k) realCb -logCallback :: HasKafkaConf k => (Int -> String -> String -> IO ()) -> k -> IO ()+-- | Add a callback for logs.+--+-- ==== __Examples__+--+-- Basic usage:+--+-- > setCallback (logCallback myLogCallback)+-- >+-- > myLogCallback :: KafkaLogLevel -> String -> String -> IO ()+-- > myLogCallback level facility message = print $ show level <> "|" <> facility <> "|" <> message+logCallback :: HasKafkaConf k => (KafkaLogLevel -> String -> String -> IO ()) -> k -> IO () logCallback callback k =- let realCb _ = callback+ let realCb _ = callback . toEnum in rdKafkaConfSetLogCb (getRdKafkaConf k) realCb +-- | Add a callback for stats.+--+-- ==== __Examples__+--+-- Basic usage:+--+-- > setCallback (statsCallback myStatsCallback)+-- >+-- > myStatsCallback :: String -> IO ()+-- > myStatsCallback stats = print $ show stats statsCallback :: HasKafkaConf k => (String -> IO ()) -> k -> IO () statsCallback callback k = let realCb _ = callback
src/Kafka/Consumer.hs view
@@ -1,8 +1,58 @@ {-# LANGUAGE LambdaCase #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TupleSections #-}++-----------------------------------------------------------------------------+-- |+-- Module to consume messages from Kafka topics.+-- +-- Here's an example of code to consume messages from a topic:+-- +-- @+-- import Control.Exception (bracket)+-- import Data.Monoid ((<>))+-- import Kafka+-- import Kafka.Consumer+-- +-- -- Global consumer properties+-- consumerProps :: 'ConsumerProperties'+-- consumerProps = 'brokersList' ['BrokerAddress' "localhost:9092"]+-- <> 'groupId' ('ConsumerGroupId' "consumer_example_group")+-- <> 'noAutoCommit'+-- <> 'logLevel' 'KafkaLogInfo'+-- +-- -- Subscription to topics+-- consumerSub :: 'Subscription'+-- consumerSub = 'topics' ['TopicName' "kafka-client-example-topic"]+-- <> 'offsetReset' 'Earliest'+-- +-- -- Running an example+-- runConsumerExample :: IO ()+-- runConsumerExample = do+-- res <- bracket mkConsumer clConsumer runHandler+-- print res+-- where+-- mkConsumer = 'newConsumer' consumerProps consumerSub+-- clConsumer (Left err) = return (Left err)+-- clConsumer (Right kc) = (maybe (Right ()) Left) \<$\> 'closeConsumer' kc+-- runHandler (Left err) = return (Left err)+-- runHandler (Right kc) = processMessages kc+-- +-- -- Example polling 10 times before stopping+-- processMessages :: 'KafkaConsumer' -> IO (Either 'KafkaError' ())+-- processMessages kafka = do+-- mapM_ (\_ -> do+-- msg1 <- 'pollMessage' kafka ('Timeout' 1000)+-- putStrLn $ "Message: " <> show msg1+-- err <- 'commitAllOffsets' 'OffsetCommit' kafka+-- putStrLn $ "Offsets: " <> maybe "Committed." show err+-- ) [0 .. 10]+-- return $ Right ()+-- @+----------------------------------------------------------------------------- module Kafka.Consumer-( module X+( KafkaConsumer+, module X , runConsumer , newConsumer , assign, assignment, subscription@@ -14,7 +64,6 @@ , storeOffsets, storeOffsetMessage , closeConsumer -- ReExport Types-, KafkaConsumer , RdKafkaRespErrT (..) ) where@@ -48,7 +97,7 @@ -- | Runs high-level kafka consumer. -- A callback provided is expected to call 'pollMessage' when convenient.-{-# DEPRECATED runConsumer "Use newConsumer/closeConsumer instead" #-}+{-# DEPRECATED runConsumer "Use 'newConsumer'/'closeConsumer' instead" #-} runConsumer :: ConsumerProperties -> Subscription -> (KafkaConsumer -> IO (Either KafkaError a)) -- ^ A callback function to poll and handle messages@@ -64,6 +113,7 @@ runHandler (Left err) = return (Left err) runHandler (Right kc) = f kc +-- | Create a `KafkaConsumer`. This consumer must be correctly released using 'closeConsumer'. newConsumer :: MonadIO m => ConsumerProperties -> Subscription@@ -94,6 +144,7 @@ runConsumerLoop kafka (Just $ Timeout 100)) >> return (Right kafka) Just err -> closeConsumer kafka >> return (Left err) +-- | Polls a single message pollMessage :: MonadIO m => KafkaConsumer -> Timeout -- ^ the timeout, in milliseconds@@ -104,7 +155,7 @@ Nothing -> rdKafkaConsumerPoll (getRdKafka c) ms >>= fromMessagePtr Just q -> rdKafkaConsumeQueue q (fromIntegral ms) >>= fromMessagePtr --- | Polls up to BatchSize messages.+-- | Polls up to 'BatchSize' messages. -- Unlike 'pollMessage' this function does not return usual "timeout" errors. -- An empty batch is returned when there are no messages available. --@@ -205,6 +256,7 @@ mapM_ (\(TopicName topicName, PartitionId partitionId) -> rdKafkaTopicPartitionListAdd pl (Text.unpack topicName) partitionId) ps KafkaResponseError <$> rdKafkaResumePartitions k pl +-- | Seek a particular offset for each provided 'TopicPartition' seek :: MonadIO m => KafkaConsumer -> Timeout -> [TopicPartition] -> m (Maybe KafkaError) seek (KafkaConsumer (Kafka k) _) (Timeout timeout) tps = liftIO $ either Just (const Nothing) <$> seekAll@@ -244,7 +296,7 @@ -- -- Events will cause application provided callbacks to be called. ----- The \p timeout_ms argument specifies the maximum amount of time+-- The 'Timeout' argument specifies the maximum amount of time -- (in milliseconds) that the call will block waiting for events. -- -- This function is called on each 'pollMessage' and, if runtime allows@@ -260,6 +312,8 @@ void . withCallbackPollEnabled k $ pollConsumerEvents' k timeout -- | Closes the consumer.+-- +-- See 'newConsumer' closeConsumer :: MonadIO m => KafkaConsumer -> m (Maybe KafkaError) closeConsumer (KafkaConsumer (Kafka k) (KafkaConf _ qr statusVar)) = liftIO $ -- because closing the consumer will raise callbacks,@@ -280,7 +334,7 @@ -- | Subscribes to a given list of topics. ----- Wildcard (regex) topics are supported by the librdkafka assignor:+-- Wildcard (regex) topics are supported by the /librdkafka/ assignor: -- any topic name in the topics list that is prefixed with @^@ will -- be regex-matched to the full list of topics in the cluster and matching -- topics will be added to the subscription list.
src/Kafka/Consumer/Subscription.hs view
@@ -17,6 +17,18 @@ import Kafka.Consumer.Types (OffsetReset (..)) import Kafka.Types (TopicName (..)) +-- | A consumer subscription to a topic.+--+-- ==== __Examples__+--+-- Typically you don't call the constructor directly, but combine settings:+--+-- @+-- consumerSub :: 'Subscription'+-- consumerSub = 'topics' ['TopicName' "kafka-client-example-topic"]+-- <> 'offsetReset' 'Earliest'+-- <> 'extraSubscriptionProps' (fromList [("prop1", "value 1"), ("prop2", "value 2")])+-- @ data Subscription = Subscription (Set TopicName) (Map Text Text) instance Sem.Semigroup Subscription where@@ -32,9 +44,11 @@ mappend = (Sem.<>) {-# INLINE mappend #-} +-- | Build a subscription by giving the list of topic names only topics :: [TopicName] -> Subscription topics ts = Subscription (Set.fromList ts) M.empty +-- | Build a subscription by giving the offset reset parameter only offsetReset :: OffsetReset -> Subscription offsetReset o = let o' = case o of@@ -42,5 +56,6 @@ Latest -> "latest" in Subscription Set.empty (M.fromList [("auto.offset.reset", o')]) +-- | Build a subscription by giving extra properties only extraSubscriptionProps :: Map Text Text -> Subscription extraSubscriptionProps = Subscription Set.empty
src/Kafka/Consumer/Types.hs view
@@ -30,7 +30,7 @@ import Data.Bifoldable (Bifoldable (..)) import Data.Bifunctor (Bifunctor (..))-import Data.Bitraversable (Bitraversable (..), bimapM, bisequenceA)+import Data.Bitraversable (Bitraversable (..), bimapM, bisequence) import Data.Int (Int64) import Data.Text (Text) import Data.Typeable (Typeable)@@ -38,6 +38,9 @@ import Kafka.Internal.Setup (HasKafka (..), HasKafkaConf (..), Kafka (..), KafkaConf (..)) import Kafka.Types (Millis (..), PartitionId (..), TopicName (..)) +-- | The main type for Kafka consumption, used e.g. to poll and commit messages.+-- +-- Its constructor is intentionally not exposed, instead, one should used 'newConsumer' to acquire such a value. data KafkaConsumer = KafkaConsumer { kcKafkaPtr :: !Kafka , kcKafkaConf :: !KafkaConf@@ -51,8 +54,17 @@ getKafkaConf = kcKafkaConf {-# INLINE getKafkaConf #-} +-- | Consumer group ID. Different consumers with the same consumer group ID will get assigned different partitions of each subscribed topic. +-- +-- See <https://kafka.apache.org/documentation/#group.id Kafka documentation on consumer group> newtype ConsumerGroupId = ConsumerGroupId { unConsumerGroupId :: Text } deriving (Show, Ord, Eq, Generic)++-- | A message offset in a partition newtype Offset = Offset { unOffset :: Int64 } deriving (Show, Eq, Ord, Read, Generic)++-- | Where to reset the offset when there is no initial offset in Kafka+-- +-- See <https://kafka.apache.org/documentation/#auto.offset.reset Kafka documentation on offset reset> data OffsetReset = Earliest | Latest deriving (Show, Eq, Generic) -- | A set of events which happen during the rebalancing process@@ -67,6 +79,7 @@ | RebalanceRevoke [(TopicName, PartitionId)] deriving (Eq, Show, Generic) +-- | The partition offset data PartitionOffset = PartitionOffsetBeginning | PartitionOffsetEnd@@ -75,11 +88,13 @@ | PartitionOffsetInvalid deriving (Eq, Show, Generic) +-- | Partitions subscribed by a consumer data SubscribedPartitions- = SubscribedPartitions [PartitionId]- | SubscribedPartitionsAll+ = SubscribedPartitions [PartitionId] -- ^ Subscribe only to those partitions+ | SubscribedPartitionsAll -- ^ Subscribe to all partitions deriving (Show, Eq, Generic) +-- | Consumer record timestamp data Timestamp = CreateTime !Millis | LogAppendTime !Millis@@ -119,8 +134,8 @@ , crPartition :: !PartitionId -- ^ Kafka partition this message was received from , crOffset :: !Offset -- ^ Offset within the 'crPartition' Kafka partition , crTimestamp :: !Timestamp -- ^ Message timestamp- , crKey :: !k- , crValue :: !v+ , crKey :: !k -- ^ Message key+ , crValue :: !v -- ^ Message value } deriving (Eq, Show, Read, Typeable, Generic) @@ -148,24 +163,27 @@ bitraverse f g r = (\k v -> bimap (const k) (const v) r) <$> f (crKey r) <*> g (crValue r) {-# INLINE bitraverse #-} +{-# DEPRECATED crMapKey "Isn't concern of this library. Use 'first'" #-} crMapKey :: (k -> k') -> ConsumerRecord k v -> ConsumerRecord k' v crMapKey = first {-# INLINE crMapKey #-} +{-# DEPRECATED crMapValue "Isn't concern of this library. Use 'second'" #-} crMapValue :: (v -> v') -> ConsumerRecord k v -> ConsumerRecord k v' crMapValue = second {-# INLINE crMapValue #-} +{-# DEPRECATED crMapKV "Isn't concern of this library. Use 'bimap'" #-} crMapKV :: (k -> k') -> (v -> v') -> ConsumerRecord k v -> ConsumerRecord k' v' crMapKV = bimap {-# INLINE crMapKV #-} -{-# DEPRECATED sequenceFirst "Isn't concern of this library. Use 'bitraverse id pure'" #-}+{-# DEPRECATED sequenceFirst "Isn't concern of this library. Use @'bitraverse' 'id' 'pure'@" #-} sequenceFirst :: (Bitraversable t, Applicative f) => t (f k) v -> f (t k v) sequenceFirst = bitraverse id pure {-# INLINE sequenceFirst #-} -{-# DEPRECATED traverseFirst "Isn't concern of this library. Use 'bitraverse f pure'" #-}+{-# DEPRECATED traverseFirst "Isn't concern of this library. Use @'bitraverse' f 'pure'@" #-} traverseFirst :: (Bitraversable t, Applicative f) => (k -> f k') -> t k v@@ -173,7 +191,7 @@ traverseFirst f = bitraverse f pure {-# INLINE traverseFirst #-} -{-# DEPRECATED traverseFirstM "Isn't concern of this library. Use 'bitraverse id pure <$> bitraverse f pure r'" #-}+{-# DEPRECATED traverseFirstM "Isn't concern of this library. Use @'bitraverse' 'id' 'pure' '<$>' 'bitraverse' f 'pure' r@" #-} traverseFirstM :: (Bitraversable t, Applicative f, Monad m) => (k -> m (f k')) -> t k v@@ -181,7 +199,7 @@ traverseFirstM f r = bitraverse id pure <$> bitraverse f pure r {-# INLINE traverseFirstM #-} -{-# DEPRECATED traverseM "Isn't concern of this library. Use 'sequenceA <$> traverse f r'" #-}+{-# DEPRECATED traverseM "Isn't concern of this library. Use @'sequenceA' '<$>' 'traverse' f r@" #-} traverseM :: (Traversable t, Applicative f, Monad m) => (v -> m (f v')) -> t v@@ -189,12 +207,12 @@ traverseM f r = sequenceA <$> traverse f r {-# INLINE traverseM #-} -{-# DEPRECATED bitraverseM "Isn't concern of this library. Use 'bisequenceA <$> bimapM f g r'" #-}+{-# DEPRECATED bitraverseM "Isn't concern of this library. Use @'bisequenceA' '<$>' 'bimapM' f g r@" #-} bitraverseM :: (Bitraversable t, Applicative f, Monad m) => (k -> m (f k')) -> (v -> m (f v')) -> t k v -> m (f (t k' v'))-bitraverseM f g r = bisequenceA <$> bimapM f g r+bitraverseM f g r = bisequence <$> bimapM f g r {-# INLINE bitraverseM #-}
src/Kafka/Internal/RdKafka.chs view
@@ -8,11 +8,12 @@ import Control.Monad (liftM) import Data.Int (Int32, Int64) import Data.Word (Word8)+import Foreign.Concurrent (newForeignPtr) import Foreign.Marshal.Alloc (alloca, allocaBytes) import Foreign.Marshal.Array (peekArray, allocaArray) import Foreign.Storable (Storable(..)) import Foreign.Ptr (Ptr, FunPtr, castPtr, nullPtr)-import Foreign.ForeignPtr (FinalizerPtr, addForeignPtrFinalizer, withForeignPtr, newForeignPtr, newForeignPtr_)+import Foreign.ForeignPtr (FinalizerPtr, addForeignPtrFinalizer, newForeignPtr_, withForeignPtr) import Foreign.C.Error (Errno(..), getErrno) import Foreign.C.String (CString, newCString, withCAString, peekCAString, peekCAStringLen, peekCString) import Foreign.C.Types (CFile, CInt(..), CSize, CChar)@@ -267,12 +268,14 @@ {`Int'} -> `RdKafkaTopicPartitionListTPtr' #} foreign import ccall unsafe "rdkafka.h &rd_kafka_topic_partition_list_destroy"- rdKafkaTopicPartitionListDestroy :: FinalizerPtr RdKafkaTopicPartitionListT+ rdKafkaTopicPartitionListDestroyF :: FinalizerPtr RdKafkaTopicPartitionListT+foreign import ccall unsafe "rdkafka.h rd_kafka_topic_partition_list_destroy"+ rdKafkaTopicPartitionListDestroy :: Ptr RdKafkaTopicPartitionListT -> IO () newRdKafkaTopicPartitionListT :: Int -> IO RdKafkaTopicPartitionListTPtr newRdKafkaTopicPartitionListT size = do ret <- rdKafkaTopicPartitionListNew size- addForeignPtrFinalizer rdKafkaTopicPartitionListDestroy ret+ addForeignPtrFinalizer rdKafkaTopicPartitionListDestroyF ret return ret {# fun rd_kafka_topic_partition_list_add as ^@@ -287,7 +290,7 @@ copyRdKafkaTopicPartitionList :: RdKafkaTopicPartitionListTPtr -> IO RdKafkaTopicPartitionListTPtr copyRdKafkaTopicPartitionList pl = do cp <- rdKafkaTopicPartitionListCopy pl- addForeignPtrFinalizer rdKafkaTopicPartitionListDestroy cp+ addForeignPtrFinalizer rdKafkaTopicPartitionListDestroyF cp return cp {# fun rd_kafka_topic_partition_list_set_offset as ^@@ -560,7 +563,7 @@ rdKafkaConsumeBatchQueue qptr timeout batchSize = do allocaArray batchSize $ \pArr -> do rSize <- rdKafkaConsumeBatchQueue' qptr timeout pArr (fromIntegral batchSize)- peekArray (fromIntegral rSize) pArr >>= traverse newForeignPtr_+ peekArray (fromIntegral rSize) pArr >>= traverse (flip newForeignPtr (return ())) ------------------------------------------------------------------------------------------------- ---- High-level KafkaConsumer@@ -582,7 +585,7 @@ (err, sub) <- rdKafkaSubscription' k case err of RdKafkaRespErrNoError ->- Right <$> newForeignPtr rdKafkaTopicPartitionListDestroy sub+ Right <$> newForeignPtr sub (rdKafkaTopicPartitionListDestroy sub) e -> return (Left e) {#fun rd_kafka_consumer_poll as ^@@ -614,7 +617,7 @@ (err, ass) <- rdKafkaAssignment' k case err of RdKafkaRespErrNoError ->- Right <$> newForeignPtr rdKafkaTopicPartitionListDestroy ass+ Right <$> newForeignPtr ass (rdKafkaTopicPartitionListDestroy ass) e -> return (Left e) {#fun rd_kafka_commit as ^@@ -721,8 +724,11 @@ -> `RdKafkaRespErrT' cIntToEnum #} foreign import ccall "rdkafka.h &rd_kafka_group_list_destroy"- rdKafkaGroupListDestroy :: FinalizerPtr RdKafkaGroupListT+ rdKafkaGroupListDestroyF :: FinalizerPtr RdKafkaGroupListT +foreign import ccall "rdkafka.h rd_kafka_group_list_destroy"+ rdKafkaGroupListDestroy :: Ptr RdKafkaGroupListT -> IO ()+ rdKafkaListGroups :: RdKafkaTPtr -> Maybe String -> Int -> IO (Either RdKafkaRespErrT RdKafkaGroupListTPtr) rdKafkaListGroups k g t = case g of Nothing -> listGroups nullPtr@@ -731,7 +737,7 @@ listGroups grp = do (err, res) <- rdKafkaListGroups' k grp t case err of- RdKafkaRespErrNoError -> Right <$> newForeignPtr rdKafkaGroupListDestroy res+ RdKafkaRespErrNoError -> Right <$> newForeignPtr res (rdKafkaGroupListDestroy res) e -> return $ Left e ------------------------------------------------------------------------------------------------- @@ -910,15 +916,15 @@ alloca- `Ptr RdKafkaMetadataT' peekPtr*, `Int'} -> `RdKafkaRespErrT' cIntToEnum #} -foreign import ccall unsafe "rdkafka.h &rd_kafka_metadata_destroy"- rdKafkaMetadataDestroy :: FinalizerPtr RdKafkaMetadataT+foreign import ccall unsafe "rdkafka.h rd_kafka_metadata_destroy"+ rdKafkaMetadataDestroy :: Ptr RdKafkaMetadataT -> IO () rdKafkaMetadata :: RdKafkaTPtr -> Bool -> Maybe RdKafkaTopicTPtr -> Int -> IO (Either RdKafkaRespErrT RdKafkaMetadataTPtr) rdKafkaMetadata k allTopics mt timeout = do tptr <- maybe (newForeignPtr_ nullPtr) pure mt (err, res) <- rdKafkaMetadata' k allTopics tptr timeout case err of- RdKafkaRespErrNoError -> Right <$> newForeignPtr rdKafkaMetadataDestroy res+ RdKafkaRespErrNoError -> Right <$> newForeignPtr res (rdKafkaMetadataDestroy res) e -> return (Left e) {#fun rd_kafka_poll as ^
src/Kafka/Producer.hs view
@@ -41,7 +41,7 @@ -- | Runs Kafka Producer. -- The callback provided is expected to call 'produceMessage' -- or/and 'produceMessageBatch' to send messages to Kafka.-{-# DEPRECATED runProducer "Use newProducer/closeProducer instead" #-}+{-# DEPRECATED runProducer "Use 'newProducer'/'closeProducer' instead" #-} runProducer :: ProducerProperties -> (KafkaProducer -> IO (Either KafkaError a)) -> IO (Either KafkaError a)
src/Kafka/Producer/Callbacks.hs view
@@ -6,11 +6,12 @@ where import Control.Monad (void)+import Control.Exception (bracket) import Control.Concurrent (forkIO) import Foreign.C.Error (getErrno) import Foreign.Ptr (Ptr, nullPtr) import Foreign.Storable (Storable(peek))-import Foreign.StablePtr (castPtrToStablePtr, deRefStablePtr)+import Foreign.StablePtr (castPtrToStablePtr, deRefStablePtr, freeStablePtr) import Kafka.Callbacks as X import Kafka.Consumer.Types (Offset(..)) import Kafka.Internal.RdKafka (RdKafkaMessageT(..), RdKafkaRespErrT(..), rdKafkaConfSetDrMsgCb)@@ -44,8 +45,8 @@ callback rep if cbPtr == nullPtr then pure ()- else do- msgCb <- deRefStablePtr @(DeliveryReport -> IO ()) $ castPtrToStablePtr $ cbPtr+ else bracket (pure $ castPtrToStablePtr cbPtr) freeStablePtr $ \stablePtr -> do+ msgCb <- deRefStablePtr @(DeliveryReport -> IO ()) stablePtr -- Here we fork the callback since it might be a longer action and -- blocking here would block librdkafka from continuing its execution void . forkIO $ msgCb rep
src/Kafka/Types.hs view
@@ -29,16 +29,27 @@ import GHC.Generics (Generic) import Kafka.Internal.RdKafka (RdKafkaRespErrT, rdKafkaErr2name, rdKafkaErr2str) +-- | Kafka broker ID newtype BrokerId = BrokerId { unBrokerId :: Int } deriving (Show, Eq, Ord, Read, Generic) +-- | Topic partition ID newtype PartitionId = PartitionId { unPartitionId :: Int } deriving (Show, Eq, Read, Ord, Enum, Generic)++-- | A number of milliseconds, used to represent durations and timestamps newtype Millis = Millis { unMillis :: Int64 } deriving (Show, Read, Eq, Ord, Num, Generic)++-- | Client ID used by Kafka to better track requests+-- +-- See <https://kafka.apache.org/documentation/#client.id Kafka documentation on client ID> newtype ClientId = ClientId { unClientId :: Text } deriving (Show, Eq, Ord, Generic)++-- | Batch size used for polling newtype BatchSize = BatchSize { unBatchSize :: Int } deriving (Show, Read, Eq, Ord, Num, Generic) +-- | Whether the topic is created by a user or by the system data TopicType = User -- ^ Normal topics that are created by user.- | System -- ^ Topics starting with "__" (__consumer_offsets, __confluent.support.metrics) are considered "system" topics+ | System -- ^ Topics starting with a double underscore "\__" (@__consumer_offsets@, @__confluent.support.metrics@, etc.) are considered "system" topics deriving (Show, Read, Eq, Ord, Generic) -- | Topic name to be consumed@@ -51,6 +62,7 @@ TopicName { unTopicName :: Text } -- ^ a simple topic name or a regex if started with @^@ deriving (Show, Eq, Ord, Read, Generic) +-- | Deduce the type of a topic from its name, by checking if it starts with a double underscore "\__" topicType :: TopicName -> TopicType topicType (TopicName tn) = if "__" `isPrefixOf` tn then System else User@@ -68,8 +80,7 @@ KafkaLogNotice | KafkaLogInfo | KafkaLogDebug deriving (Show, Enum, Eq) ------ | Any Kafka errors+-- | All possible Kafka errors data KafkaError = KafkaError Text | KafkaInvalidReturnValue@@ -85,6 +96,7 @@ "[" ++ rdKafkaErr2name err ++ "] " ++ rdKafkaErr2str err displayException err = show err +-- | Available /librdkafka/ debug contexts data KafkaDebug = DebugGeneric | DebugBroker@@ -100,6 +112,9 @@ | DebugAll deriving (Eq, Show, Typeable, Generic) +-- | Convert a 'KafkaDebug' into its /librdkafka/ string equivalent.+--+-- This is used internally by the library but may be useful to some developers. kafkaDebugToText :: KafkaDebug -> Text kafkaDebugToText d = case d of DebugGeneric -> "generic"@@ -115,6 +130,9 @@ DebugFeature -> "feature" DebugAll -> "all" +-- | Compression codec used by a topic+--+-- See <https://kafka.apache.org/documentation/#compression.type Kafka documentation on compression codecs> data KafkaCompressionCodec = NoCompression | Gzip@@ -122,6 +140,9 @@ | Lz4 deriving (Eq, Show, Typeable, Generic) +-- | Convert a 'KafkaCompressionCodec' into its /librdkafka/ string equivalent.+--+-- This is used internally by the library but may be useful to some developers. kafkaCompressionCodecToText :: KafkaCompressionCodec -> Text kafkaCompressionCodecToText c = case c of NoCompression -> "none"
tests-it/Kafka/IntegrationSpec.hs view
@@ -5,22 +5,21 @@ module Kafka.IntegrationSpec where -import Control.Monad (forM, forM_)-import Control.Monad.Loops-import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)-import qualified Data.ByteString as BS-import Data.Either-import Data.Map (fromList)-import Data.Monoid ((<>))--import Kafka.Consumer as C-import Kafka.Metadata as M-import Kafka.Producer as P-+import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)+import Control.Monad (forM, forM_)+import Control.Monad.Loops+import Data.Either+import Data.Map (fromList)+import Data.Monoid ((<>))+import Kafka.Consumer+import Kafka.Metadata+import Kafka.Producer import Kafka.TestEnv import Test.Hspec -{-# ANN module ("HLint: ignore Redundant do" :: String) #-}+import qualified Data.ByteString as BS++{- HLINT ignore "Redundant do" -} spec :: Spec spec = do