packages feed

kafka-effectful-0.3.1.0: src/Kafka/Effectful/Consumer/Effect.hs

module Kafka.Effectful.Consumer.Effect
  ( -- * Effect
    KafkaConsumer (..),

    -- * Polling
    pollMessage,
    pollMessageEither,
    pollMessageBatch,

    -- * Offset Management
    commitOffsetMessage,
    commitAllOffsets,
    commitPartitionsOffsets,
    storeOffsets,
    storeOffsetMessage,

    -- * Partition Management
    assign,
    pausePartitions,
    resumePartitions,
    seekPartitions,

    -- * Querying
    committed,
    position,
    assignment,
    subscription,

    -- * Internal — cross-effect plumbing
    askConsumerHandle,
  )
where

import Data.ByteString (ByteString)
import Data.Map.Strict (Map)
import Effectful (Dispatch (..), DispatchOf, Eff, Effect, (:>))
import Effectful.Dispatch.Dynamic (send)
import Kafka.Consumer.Types
  ( ConsumerRecord,
    OffsetCommit,
    SubscribedPartitions,
    TopicPartition,
  )
import Kafka.Consumer.Types qualified as KC
import Kafka.Types
  ( BatchSize,
    KafkaError,
    PartitionId,
    Timeout,
    TopicName,
  )

-- | Effect for Kafka consumer operations.
data KafkaConsumer :: Effect where
  PollMessage ::
    Timeout ->
    KafkaConsumer m (Maybe (ConsumerRecord (Maybe ByteString) (Maybe ByteString)))
  PollMessageEither ::
    Timeout ->
    KafkaConsumer m (Either KafkaError (ConsumerRecord (Maybe ByteString) (Maybe ByteString)))
  PollMessageBatch ::
    Timeout ->
    BatchSize ->
    KafkaConsumer m [Either KafkaError (ConsumerRecord (Maybe ByteString) (Maybe ByteString))]
  CommitOffsetMessage ::
    OffsetCommit ->
    ConsumerRecord k v ->
    KafkaConsumer m ()
  CommitAllOffsets ::
    OffsetCommit ->
    KafkaConsumer m ()
  CommitPartitionsOffsets ::
    OffsetCommit ->
    [TopicPartition] ->
    KafkaConsumer m ()
  StoreOffsets ::
    [TopicPartition] ->
    KafkaConsumer m ()
  StoreOffsetMessage ::
    ConsumerRecord k v ->
    KafkaConsumer m ()
  Assign ::
    [TopicPartition] ->
    KafkaConsumer m ()
  PausePartitions ::
    [(TopicName, PartitionId)] ->
    KafkaConsumer m ()
  ResumePartitions ::
    [(TopicName, PartitionId)] ->
    KafkaConsumer m ()
  SeekPartitions ::
    [TopicPartition] ->
    Timeout ->
    KafkaConsumer m ()
  Committed ::
    Timeout ->
    [(TopicName, PartitionId)] ->
    KafkaConsumer m [TopicPartition]
  Position ::
    [(TopicName, PartitionId)] ->
    KafkaConsumer m [TopicPartition]
  Assignment ::
    KafkaConsumer m (Map TopicName [PartitionId])
  Subscription ::
    KafkaConsumer m [(TopicName, SubscribedPartitions)]
  AskConsumerHandle ::
    KafkaConsumer m KC.KafkaConsumer

type instance DispatchOf KafkaConsumer = 'Dynamic

-- Polling

-- | Poll for a single message.
--
-- Returns 'Nothing' when nothing was delivered, which covers the timeout and
-- three partition-scoped conditions that librdkafka reports as errors but which
-- a healthy consumer is expected to meet during normal operation:
--
-- * @RdKafkaRespErrPartitionEof@ — caught up with a partition.
-- * @RdKafkaRespErrAutoOffsetReset@ — the position was reset, or a reset was
--   refused. Raised as a consumer error only under @auto.offset.reset=error@ or
--   when a reset itself fails; a successful reset after retention loss only
--   logs.
-- * @RdKafkaRespErrUnknownTopicOrPart@ — the topic or partition is not known
--   yet, normal inside a topic-creation window.
--
-- Every other in-band error is thrown as 'KafkaError' via the 'Error' effect —
-- transport failures, authentication failures, and fatal errors among them.
--
-- Use 'pollMessageEither' when you need to observe the swallowed conditions,
-- for example to detect partition EOF in a bounded read. The full policy lives
-- in "Kafka.Effectful.Consumer.Classify".
pollMessage ::
  (KafkaConsumer :> es) =>
  Timeout ->
  Eff es (Maybe (ConsumerRecord (Maybe ByteString) (Maybe ByteString)))
pollMessage = send . PollMessage

-- | Poll for a single message, returning every in-band condition as a
-- 'Left' instead of swallowing or throwing it.
--
-- Nothing is hidden: timeouts, partition EOF, offset resets and hard failures
-- all arrive as @Left@. Use this for bounded reads that must observe partition
-- EOF to know when to stop, or wherever the full librdkafka taxonomy matters.
--
-- @since 0.4.0.0
pollMessageEither ::
  (KafkaConsumer :> es) =>
  Timeout ->
  Eff es (Either KafkaError (ConsumerRecord (Maybe ByteString) (Maybe ByteString)))
pollMessageEither = send . PollMessageEither

-- | Poll for a batch of messages. Per-message errors are preserved in the 'Either'.
pollMessageBatch ::
  (KafkaConsumer :> es) =>
  Timeout ->
  BatchSize ->
  Eff es [Either KafkaError (ConsumerRecord (Maybe ByteString) (Maybe ByteString))]
pollMessageBatch t b = send $ PollMessageBatch t b

-- Offset Management

-- | Commit the offset of a specific message. Throws 'KafkaError' on failure.
--
-- A commit that finds nothing to commit (@RdKafkaRespErrNoOffset@) is a
-- success, not a failure — see "Kafka.Effectful.Consumer.Classify".
commitOffsetMessage ::
  (KafkaConsumer :> es) => OffsetCommit -> ConsumerRecord k v -> Eff es ()
commitOffsetMessage oc cr = send $ CommitOffsetMessage oc cr

-- | Commit offsets for all currently assigned partitions. Throws
-- 'KafkaError' on failure.
--
-- An idle consumer with nothing to commit succeeds rather than throwing; see
-- 'commitOffsetMessage'.
commitAllOffsets :: (KafkaConsumer :> es) => OffsetCommit -> Eff es ()
commitAllOffsets = send . CommitAllOffsets

-- | Commit offsets for specific partitions. Throws 'KafkaError' on failure.
--
-- A commit with nothing to commit succeeds; see 'commitOffsetMessage'.
commitPartitionsOffsets ::
  (KafkaConsumer :> es) => OffsetCommit -> [TopicPartition] -> Eff es ()
commitPartitionsOffsets oc tps = send $ CommitPartitionsOffsets oc tps

-- | Store offsets locally without committing to the broker. Throws 'KafkaError' on failure.
storeOffsets :: (KafkaConsumer :> es) => [TopicPartition] -> Eff es ()
storeOffsets = send . StoreOffsets

-- | Store a message's offset locally without committing. Throws 'KafkaError' on failure.
storeOffsetMessage :: (KafkaConsumer :> es) => ConsumerRecord k v -> Eff es ()
storeOffsetMessage = send . StoreOffsetMessage

-- Partition Management

-- | Manually assign partitions to the consumer. Throws 'KafkaError' on failure.
assign :: (KafkaConsumer :> es) => [TopicPartition] -> Eff es ()
assign = send . Assign

-- | Pause consuming from the specified partitions. Throws 'KafkaError' on failure.
pausePartitions :: (KafkaConsumer :> es) => [(TopicName, PartitionId)] -> Eff es ()
pausePartitions = send . PausePartitions

-- | Resume consuming from the specified partitions. Throws 'KafkaError' on failure.
resumePartitions :: (KafkaConsumer :> es) => [(TopicName, PartitionId)] -> Eff es ()
resumePartitions = send . ResumePartitions

-- | Seek to specific offsets for partitions. Throws 'KafkaError' on failure.
seekPartitions :: (KafkaConsumer :> es) => [TopicPartition] -> Timeout -> Eff es ()
seekPartitions tps t = send $ SeekPartitions tps t

-- Querying

-- | Get committed offsets for the specified partitions. Throws 'KafkaError' on failure.
committed ::
  (KafkaConsumer :> es) =>
  Timeout ->
  [(TopicName, PartitionId)] ->
  Eff es [TopicPartition]
committed t ps = send $ Committed t ps

-- | Get the current position (last consumed offset + 1). Throws 'KafkaError' on failure.
position ::
  (KafkaConsumer :> es) =>
  [(TopicName, PartitionId)] ->
  Eff es [TopicPartition]
position = send . Position

-- | Get the current partition assignment.
assignment :: (KafkaConsumer :> es) => Eff es (Map TopicName [PartitionId])
assignment = send Assignment

-- | Get the current topic subscription.
subscription :: (KafkaConsumer :> es) => Eff es [(TopicName, SubscribedPartitions)]
subscription = send Subscription

-- | Escape hatch: return the raw @Kafka.Consumer.KafkaConsumer@ handle
-- acquired by @runKafkaConsumer@.
--
-- Exposed to enable the cross-effect
-- 'Kafka.Effectful.Producer.Transaction.commitOffsetMessageTransaction'
-- helper, which must reach both the producer and consumer handles to
-- call the underlying transactional offset-commit primitive. New
-- operations should go through the 'KafkaConsumer' effect rather than
-- this handle.
--
-- @since 0.2.0.0
askConsumerHandle :: (KafkaConsumer :> es) => Eff es KC.KafkaConsumer
askConsumerHandle = send AskConsumerHandle