packages feed

rdkafka-hs-1.1.0.0: test-integration/Kafka/Integration/ConsumerTest.hs

-- The inherited seek checks cover the deprecated seek.
{-# OPTIONS_GHC -Wno-deprecations #-}

-- | The inherited main consumer scenario: produce, then run the same consumer
-- checks once per callback poll mode.
module Kafka.Integration.ConsumerTest
  ( tests,
  )
where

import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
import Control.Exception (bracket, throwIO)
import Control.Monad (forM_)
import Data.Either (isRight)
import Data.Map (fromList)
import Kafka.Consumer
import Kafka.Metadata
import Kafka.Producer
import Kafka.TestEnv
import Test.Tasty (DependencyType (AllSucceed), TestTree, dependentTestGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))

tests :: TestEnv -> TestTree
tests env =
  dependentTestGroup
    "Consumer"
    AllSucceed
    [ withProducer env $ \getProducer ->
        dependentTestGroup
          "Run producer"
          AllSucceed
          [ testCase "sends messages to test topic" $ do
              res <- getProducer >>= sendMessages (testMessages (teTopic env))
              res @?= Right (),
            testCase "sends messages with callback to test topic" $ do
              prod <- getProducer
              var <- newEmptyMVar
              let msg =
                    ProducerRecord
                      { prTopic = TopicName (prefixedName env "callback-topic"),
                        prPartition = UnassignedPartition,
                        prKey = Nothing,
                        prValue = Just "test from producer",
                        prHeaders = mempty
                      }

              res <- produceMessage' prod msg (putMVar var)
              res @?= Right ()
              callbackRes <- flushProducer prod *> takeMVar var
              case callbackRes of
                DeliverySuccess _ _ -> pure ()
                DeliveryFailure _ err -> assertFailure ("delivery failed: " <> show err)
                NoMessageError err -> assertFailure ("no message: " <> show err)
          ],
      withConsumer env (consumerProps env <> groupId (makeGroupId env "async")) $
        dependentTestGroup "Run consumer with async polling" AllSucceed . consumerChecks env CallbackPollModeAsync,
      withConsumer env (consumerProps env <> groupId (makeGroupId env "sync") <> callbackPollMode CallbackPollModeSync) $
        dependentTestGroup "Run consumer with sync polling" AllSucceed . consumerChecks env CallbackPollModeSync
    ]

consumerChecks :: TestEnv -> CallbackPollMode -> IO KafkaConsumer -> [TestTree]
consumerChecks env mode getConsumer =
  [ check "should receive messages" $ \k -> do
      res <- receiveTestMessages env k 14 0
      length <$> res @?= Right 14

      let timestamps = crTimestamp <$> either (const []) id res
      forM_ timestamps $ \ts ->
        assertBool "expected a timestamp" (ts /= NoTimestamp)

      comRes <- commitAllOffsets OffsetCommit k
      comRes @?= Nothing,
    check "should get committed" $ \k -> do
      res <- committed k (Timeout 1000) [(testTopic, PartitionId 0)]
      assertBool (show res) (isRight res),
    check "should get position" $ \k -> do
      res <- position k [(testTopic, PartitionId 0)]
      assertBool (show res) (isRight res),
    check "should get watermark offsets" $ \k -> do
      res <- sequence <$> watermarkOffsets k (Timeout 1000) testTopic
      assertBool (show res) (isRight res)
      length <$> res @?= Right 1,
    check "should return subscription" $ \k -> do
      res <- subscription k
      assertBool (show res) (isRight res)
      length <$> res @?= Right 1,
    check "should return assignment" $ \k -> do
      res <- assignment k
      res @?= Right (fromList [(testTopic, [PartitionId 0])]),
    check "assignment is not lost" $ \k ->
      assignmentLost k >>= (@?= False),
    check "should return all topics metadata" $ \k -> do
      res <- allTopicsMetadata k (Timeout 1000)
      assertBool (show res) (isRight res)
      let filterUserTopics m = m {kmTopics = filter (\t -> topicType (tmTopicName t) == User) (kmTopics m)}
      let res' = fmap filterUserTopics res
      length . kmBrokers <$> res' @?= Right 1

      let topicsLen = either (const 0) (length . kmTopics) res'
      let hasTopic = either (const False) (any (\t -> tmTopicName t == testTopic) . kmTopics) res'

      assertBool "expected user topics" (topicsLen > 0)
      hasTopic @?= True,
    check "should return topic metadata" $ \k -> do
      res <- topicMetadata k (Timeout 2000) testTopic
      assertBool (show res) (isRight res)
      length . kmBrokers <$> res @?= Right 1
      length . kmTopics <$> res @?= Right 1,
    check "should describe all consumer groups" $ \k -> do
      res <- allConsumerGroupsInfo k (Timeout 2000)
      assertBool (show res) (isRight res)
      let groups = either (const []) (fmap giGroup) res
      let prefixedGroups = filter (isTestGroupId env) groups
      assertBool "expected this run's consumer groups" (not (null prefixedGroups)),
    check "should describe a given consumer group" $ \k -> do
      res <- consumerGroupInfo k (Timeout 2000) (testGroupId env)
      fmap giGroup <$> res @?= Right [testGroupId env],
    check "should describe non-existent consumer group" $ \k -> do
      res <- consumerGroupInfo k (Timeout 2000) (makeGroupId env "does-not-exist")
      res @?= Right [],
    check "should read topic offsets for time" $ \k -> do
      res <- topicOffsetsForTime k (Timeout 2000) (Millis 1904057189508) testTopic
      assertBool (show res) (isRight res)
      fmap tpOffset <$> res @?= Right [PartitionOffsetEnd],
    checkSeek "should seek and return no error" "offset" $ \seekTopic k -> do
      res <- seek k (Timeout 1000) [TopicPartition seekTopic (PartitionId 0) (PartitionOffset 1)]
      res @?= Nothing
      msg <- pollMessage k (Timeout 1000)
      fmap (\m -> (crTopic m, crPartition m, crOffset m, crValue m)) msg @?= Right (seekTopic, PartitionId 0, Offset 1, Just "one"),
    checkSeek "should seek to the beginning" "beginning" $ \seekTopic k -> do
      res <- seek k (Timeout 1000) [TopicPartition seekTopic (PartitionId 0) PartitionOffsetBeginning]
      res @?= Nothing
      msg <- pollMessage k (Timeout 1000)
      fmap (\m -> (crTopic m, crPartition m, crOffset m, crValue m)) msg @?= Right (seekTopic, PartitionId 0, Offset 0, Just "zero"),
    checkSeek "should seek to the end" "end" $ \seekTopic k -> do
      res <- seek k (Timeout 1000) [TopicPartition seekTopic (PartitionId 0) PartitionOffsetEnd]
      res @?= Nothing
      msg <- pollMessage k (Timeout 1000)
      let offset = crOffset <$> msg
      assertBool
        ("expected partition EOF or a timeout, got " <> show offset)
        ( offset == Left (KafkaResponseError RdKafkaRespErrPartitionEof)
            || offset == Left (KafkaResponseError RdKafkaRespErrTimedOut)
        ),
    checkSeek "should respect out-of-bound offsets (invalid offset)" "invalid" $ \seekTopic k -> do
      res <- seek k (Timeout 2000) [TopicPartition seekTopic (PartitionId 0) PartitionOffsetInvalid]
      res @?= Nothing
      msg <- pollMessage k (Timeout 1000)
      fmap (\m -> (crTopic m, crPartition m, crOffset m, crValue m)) msg @?= Right (seekTopic, PartitionId 0, Offset 0, Just "zero"),
    checkSeek "should respect out-of-bound offsets (huge offset)" "huge" $ \seekTopic k -> do
      res <- seek k (Timeout 3000) [TopicPartition seekTopic (PartitionId 0) (PartitionOffset 123456)]
      res @?= Nothing
      msg <- pollMessage k (Timeout 2000)
      fmap (\m -> (crTopic m, crPartition m, crOffset m, crValue m)) msg @?= Right (seekTopic, PartitionId 0, Offset 0, Just "zero")
  ]
  where
    testTopic = teTopic env
    check name f = testCase name (getConsumer >>= f)

    -- A previous assignment query is a snapshot, not proof that a fetcher is
    -- still active after unrelated blocking metadata operations. Each positive
    -- seek begins immediately after this fresh grouped consumer reads record 0.
    checkSeek name suffix action = testCase name $
      withTopic env ("legacy-seek-" <> label <> "-" <> suffix) 1 $ \topic producer -> do
        forM_ ["zero", "one", "two"] (produceTo producer topic 0)
        let props = consumerProps env <> groupId (makeGroupId env ("legacy-seek-" <> label <> "-" <> suffix)) <> noAutoOffsetStore <> callbackPollMode mode
        bracket (mkConsumerFor topic props) releaseConsumer $ \consumer -> do
          first <- pollMessage consumer (Timeout 10000) >>= either throwIO pure
          (crTopic first, crPartition first, crOffset first, crValue first) @?= (topic, PartitionId 0, Offset 0, Just "zero")
          action topic consumer
    label = if mode == CallbackPollModeSync then "sync" else "async"