packages feed

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

module Kafka.Integration.ConsumerPropertyTest (tests) where

import Control.Concurrent.MVar (newEmptyMVar, tryPutMVar, tryReadMVar)
import Control.Exception (bracket, finally, throwIO)
import Control.Monad (void)
import Data.Int (Int64)
import Data.Text qualified as Text
import Kafka.Consumer
import Kafka.FixtureSupport (acquirePair)
import Kafka.Producer qualified as P
import Kafka.PropertySupport
import Kafka.TestEnv (TestEnv, makeGroupId, produceTo, releaseConsumer, releaseProducer, teBrokers, withTopic)
import Kafka.Transaction qualified as T
import Kafka.TransactionTestSupport
import System.Mem (performMajorGC)
import System.Timeout (timeout)
import Test.Tasty (TestTree, withResource)
import Test.Tasty.HUnit (assertFailure, testCase, (@?=))
import Test.Tasty.QuickCheck qualified as QC

tests :: TestEnv -> TestTree
tests testEnv =
  generatedGroup
    "consumer-properties"
    $ [ QC.testProperty "consumer callback composition preserves registration order and identity" $
          QC.forAllShrink programs shrinkPrograms $ \input@(a, b, c, leftAssociated, mode) -> QC.ioProperty $ do
            result <- observe a b c leftAssociated mode
            performMajorGC
            let labels = a <> b <> c
                expected = case reverse labels of [] -> Nothing; value : _ -> Just value
            pure (QC.counterexample (show input) (result QC.=== (length labels, expected))),
        withResource offsetClients releaseOffsetClients $ \clients ->
          QC.testProperty "generated source offsets reach native code as the representable next position" $
            QC.forAllShrink nextOffset (filter (>= 0) . QC.shrink) $ \n -> QC.ioProperty $ do
              (producer, consumer) <- clients
              withTxFault 100 $ do
                let message = ConsumerRecord (TopicName "generated-source-offset") (PartitionId 0) (Offset n) NoTimestamp mempty () ()
                result <- T.commitOffsetMessageTransaction producer consumer message (Timeout 1000)
                actualOffset <- txStat 12
                created <- txStat 0
                destroyed <- txStat 1
                metadata <- txStat 6
                freedMetadata <- txStat 7
                afterFree <- txStat 8
                pure $
                  (fmap T.getKafkaError result, actualOffset, created, destroyed, metadata, freedMetadata, afterFree)
                    QC.=== (Just (KafkaResponseError RdKafkaRespErrState), n + 1, 1, 1, 1, 1, 0)
      ]
      <> [ testCase (label <> " later subscription reset reaches the native consumer") $
             withTopic testEnv (Text.pack ("subscription-last-" <> label)) 1 $ \topic producer -> do
               produceTo producer topic 0 "existing"
               let props = brokersList (teBrokers testEnv) <> groupId (makeGroupId testEnv (Text.pack ("subscription-last-" <> label))) <> noAutoCommit <> callbackPollMode mode
               bracket (newConsumer props (topics [topic] <> offsetReset Latest <> offsetReset Earliest) >>= either throwIO pure) releaseConsumer $ \consumer -> do
                 record <- pollMessage consumer (Timeout 10000) >>= either throwIO pure
                 (crOffset record, crValue record) @?= (Offset 0, Just "existing")
         | (label, mode) <- [("async", CallbackPollModeAsync), ("sync", CallbackPollModeSync)]
         ]

-- Public records consumed from Kafka have nonnegative offsets; generation is
-- restricted to offsets whose next position fits the native signed field.
nextOffset :: QC.Gen Int64
nextOffset = QC.frequency [(1, QC.elements [0, 1, maxBound - 1]), (4, QC.choose (0, maxBound - 1))]

offsetClients :: IO (P.KafkaProducer, KafkaConsumer)
offsetClients =
  acquirePair
    (P.newProducer (P.brokersList ["127.0.0.1:1"] <> P.extraProp "log_level" "0") >>= either throwIO pure)
    releaseProducer
    (newAssignedConsumer (brokersList ["127.0.0.1:1"] <> groupId "generated-offset-unused-group" <> callbackPollMode CallbackPollModeSync <> extraProp "log_level" "0") >>= either throwIO pure)

releaseOffsetClients :: (P.KafkaProducer, KafkaConsumer) -> IO ()
releaseOffsetClients (producer, consumer) = destroyConsumer consumer `finally` releaseProducer producer

programs :: QC.Gen ([Int], [Int], [Int], Bool, CallbackPollMode)
programs = (,,,,) <$> labels <*> labels <*> labels <*> QC.arbitrary <*> QC.elements [CallbackPollModeAsync, CallbackPollModeSync]
  where
    labels = boundedList 6 (QC.choose (0, 5))

shrinkPrograms :: ([Int], [Int], [Int], Bool, CallbackPollMode) -> [([Int], [Int], [Int], Bool, CallbackPollMode)]
shrinkPrograms (a, b, c, association, mode) =
  [(a', b, c, association, mode) | a' <- QC.shrinkList QC.shrink a]
    <> [(a, b', c, association, mode) | b' <- QC.shrinkList QC.shrink b]
    <> [(a, b, c', association, mode) | c' <- QC.shrinkList QC.shrink c]
    <> [(a, b, c, False, mode) | association]

{-# NOINLINE observe #-}
observe :: [Int] -> [Int] -> [Int] -> Bool -> CallbackPollMode -> IO (Int, Maybe Int)
observe a b c association mode = do
  called <- newEmptyMVar
  let callback label = logCallback (\_ _ _ -> void (tryPutMVar called label))
      callbacks = foldMap (setCallback . callback . Just)
      p = callbacks a
      q = callbacks b
      r = callbacks c
      composed = if association then ((p <> mempty) <> q) <> r else p <> (mempty <> (q <> r))
      props =
        brokersList [BrokerAddress "127.0.0.1:1"]
          <> groupId (ConsumerGroupId "consumer-property-unused-group")
          <> extraProp "debug" "generic"
          <> extraProp "log_level" "7"
          <> callbackPollMode mode
          <> setCallback (callback Nothing)
          <> composed
  bracket (newAssignedConsumer props >>= either throwIO pure) destroyConsumer $ \consumer -> do
    let poll =
          tryReadMVar called >>= \case
            Just value -> pure value
            Nothing -> pollConsumerEvents consumer (Just (Timeout 50)) >> poll
    label <- timeout 5000000 poll >>= maybe (assertFailure "generated consumer callback did not run") pure
    pure (length (cpCallbacks composed), label)