packages feed

haskakafka-1.1.0: src/Haskakafka/Consumer.hs

module Haskakafka.Consumer
( runConsumerConf
, runConsumer
, newKafkaConsumerConf
, newKafkaConsumer
, setRebalanceCallback
, assign
, subscribe
, pollMessage
, closeConsumer
, setOffsetStore

-- Types
, CIT.ConsumerGroupId (..)
, CIT.TopicName (..)
, CIT.BrokersString (..)
, CIT.KafkaTopicPartition (..)
)
where

import           Control.Exception
import           Foreign
import           Haskakafka
import           Haskakafka.Consumer.Internal.Convert
import           Haskakafka.Consumer.Internal.Types
import           Haskakafka.InternalRdKafka
import           Haskakafka.InternalRdKafkaEnum
import           Haskakafka.InternalSetup
import           Haskakafka.InternalShared
import           Haskakafka.InternalTypes

import qualified Haskakafka.Consumer.Internal.Types   as CIT

-- | Runs high-level kafka consumer.
--
-- A callback provided is expected to call 'pollMessage' when convenient.
runConsumerConf :: KafkaConf                            -- ^ Consumer config (see 'newKafkaConsumerConf')
                -> BrokersString                        -- ^ Comma separated list of brokers with ports (e.g. @localhost:9092@)
                -> [TopicName]                          -- ^ List of topics to be consumed
                -> (Kafka -> IO (Either KafkaError ())) -- ^ A callback function to poll and handle messages
                -> IO (Either KafkaError ())
runConsumerConf c bs ts f =
    bracket mkConsumer clConsumer runHandler
    where
        mkConsumer = do
            kafka <- newKafkaConsumer bs c
            _ <- setHlConsumer kafka
            sErr  <- subscribe kafka ts
            return $ if hasError sErr
                         then Left (sErr, kafka)
                         else Right kafka

        clConsumer (Left (_, kafka)) = kafkaErrorToEither <$> closeConsumer kafka
        clConsumer (Right kafka) = kafkaErrorToEither <$> closeConsumer kafka

        runHandler (Left (err, _)) = return $ Left err
        runHandler (Right kafka) = f kafka

-- | Runs high-level kafka consumer.
--
-- A callback provided is expected to call 'pollMessage' when convenient.
runConsumer :: ConsumerGroupId                       -- ^ Consumer group id (a @group.id@ property of a kafka consumer)
             -> ConfigOverrides                      -- ^ Extra kafka consumer parameters (see kafka documentation)
             -> BrokersString                        -- ^ Comma separated list of brokers with ports (e.g. @localhost:9092@)
             -> [TopicName]                          -- ^ List of topics to be consumed
             -> (Kafka -> IO (Either KafkaError ())) -- ^ A callback function to poll and handle messages
             -> IO (Either KafkaError ())
runConsumer g c bs ts f = do
    conf <- newKafkaConsumerConf g c
    runConsumerConf conf bs ts f

-- | Creates a new kafka configuration for a consumer with a specified 'ConsumerGroupId'.
newKafkaConsumerConf :: ConsumerGroupId  -- ^ Consumer group id (a @group.id@ property of a kafka consumer)
                     -> ConfigOverrides  -- ^ Extra kafka consumer parameters (see kafka documentation)
                     -> IO KafkaConf     -- ^ Kafka configuration which can be altered before it is used in 'newKafkaConsumer'
newKafkaConsumerConf (ConsumerGroupId gid) conf = do
    kc <- kafkaConf conf
    setKafkaConfValue kc "group.id" gid
    return kc

-- | Creates a new kafka consumer
newKafkaConsumer :: BrokersString -- ^ Comma separated list of brokers with ports (e.g. @localhost:9092@)
                 -> KafkaConf     -- ^ Kafka configuration for a consumer (see 'newKafkaConsumerConf')
                 -> IO Kafka      -- ^ Kafka instance
newKafkaConsumer (BrokersString bs) conf = do
    kafka <- newKafkaPtr RdKafkaConsumer conf
    addBrokers kafka bs
    return kafka

-- | Sets a callback that is called when rebalance is needed.
--
-- Callback implementations suppose to watch for 'KafkaResponseError' 'RdKafkaRespErrAssignPartitions' and
-- for 'KafkaResponseError' 'RdKafkaRespErrRevokePartitions'. Other error codes are not expected and would indicate
-- something really bad happening in a system, or bugs in @librdkafka@ itself.
--
-- A callback is expected to call 'assign' according to the error code it receives.
--
--     * When 'RdKafkaRespErrAssignPartitions' happens 'assign' should be called with all the partitions it was called with.
--       It is OK to alter partitions offsets before calling 'assign'.
--
--     * When 'RdKafkaRespErrRevokePartitions' happens 'assign' should be called with an empty list of partitions.
setRebalanceCallback :: KafkaConf
                     -> (Kafka -> KafkaError -> [KafkaTopicPartition] -> IO ())
                     -> IO ()
setRebalanceCallback (KafkaConf conf) callback = rdKafkaConfSetRebalanceCb conf realCb
  where
    realCb :: Ptr RdKafkaT -> RdKafkaRespErrT -> Ptr RdKafkaTopicPartitionListT -> Ptr Word8 -> IO ()
    realCb rk err pl _ = do
        rk' <- newForeignPtr_ rk
        pl' <- peek pl
        ps  <- fromNativeTopicPartitionList pl'
        callback (Kafka rk' (KafkaConf conf)) (KafkaResponseError err) ps

-- | Assigns specified partitions to a current consumer.
-- Assigning an empty list means unassigning from all partitions that are currently assigned.
-- See 'setRebalanceCallback' for more details.
assign :: Kafka -> [KafkaTopicPartition] -> IO KafkaError
assign (Kafka k _) ps =
    let pl = if null ps
                then newForeignPtr_ nullPtr
                else toNativeTopicPartitionList ps
    in  KafkaResponseError <$> (pl >>= rdKafkaAssign k)

-- | Subscribes to a given list of topics.
--
-- 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.
subscribe :: Kafka -> [TopicName] -> IO KafkaError
subscribe (Kafka k _) ts = do
    pl <- newRdKafkaTopicPartitionListT (length ts)
    mapM_ (\(TopicName t) -> rdKafkaTopicPartitionListAdd pl t (-1)) ts
    KafkaResponseError <$> rdKafkaSubscribe k pl

-- | Closes the consumer and destroys it.
closeConsumer :: Kafka -> IO KafkaError
closeConsumer (Kafka k _) = KafkaResponseError <$> rdKafkaConsumerClose k

-----------------------------------------------------------------------------
setTopicValue :: KafkaTopic -> String -> String -> IO ()
setTopicValue (KafkaTopic _ _ conf) = setKafkaTopicConfValue conf

pollMessage :: Kafka
               -> Int -- ^ the timeout, in milliseconds (@10^3@ per second)
               -> IO (Either KafkaError KafkaMessage) -- ^ Left on error or timeout, right for success
pollMessage (Kafka k _) timeout =
    rdKafkaConsumerPoll k (fromIntegral timeout) >>= fromMessagePtr

-- | Redirects 'consumeMessage' to poll. Implementation details.
setHlConsumer :: Kafka -> IO KafkaError
setHlConsumer (Kafka k _) = KafkaResponseError <$> rdKafkaPollSetConsumer k

-- | Sets the offset store for a specified topic.
-- @librdkafka@ supports both @broker@ and @file@ but it seems that consumers with groups
-- can only support @broker@. Which is good and enough.
setOffsetStore :: KafkaTopic -> OffsetStoreMethod -> IO ()
setOffsetStore t o =
    let setValue = setTopicValue t
    in  case o of
          OffsetStoreBroker ->
              setValue "offset.store.method" "broker"

          OffsetStoreFile path sync -> do
              setValue "offset.store.method" "file"
              setValue "offset.store.file" path
              setValue "offset.store.sync.interval.ms" (show $ offsetSyncToInt sync)