packages feed

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

{-# OPTIONS_GHC -Wno-deprecations #-}

module Kafka.Integration.DeprecatedRunnerContractTest (tests) where

import Control.Concurrent.MVar (newEmptyMVar, tryPutMVar, tryTakeMVar)
import Control.Exception (Exception, finally, throwIO, try)
import Control.Monad (void, when)
import Data.IORef (newIORef, readIORef, writeIORef)
import Data.Map.Strict qualified as M
import Data.Text qualified as Text
import Kafka.Consumer qualified as C
import Kafka.Consumer.Types qualified as CT
import Kafka.Dump (dumpKafkaConf, dumpTopicConf)
import Kafka.Metadata (kmTopics, tmTopicName, topicMetadata)
import Kafka.Producer qualified as P
import Kafka.Producer.Types qualified as PT
import Kafka.TestEnv
import Test.Tasty (TestTree, testGroup)
import Test.Tasty.HUnit (assertBool, assertFailure, testCase, (@?=))

data Outcome = Returned | Refused | Thrown deriving (Eq, Show)

data HandlerFailure = HandlerFailure deriving (Eq, Show)

instance Exception HandlerFailure

marker :: C.KafkaError
marker = C.KafkaBadSpecification "runner-handler-result"

finish :: Outcome -> IO (Either C.KafkaError Int)
finish Returned = pure (Right 42)
finish Refused = pure (Left marker)
finish Thrown = throwIO HandlerFailure

checkResult :: Outcome -> Either HandlerFailure (Either C.KafkaError Int) -> IO ()
checkResult Returned result = result @?= Right (Right 42)
checkResult Refused result = result @?= Right (Left marker)
checkResult Thrown result = result @?= Left HandlerFailure

tests :: TestEnv -> TestTree
tests env =
  testGroup "deprecated-runners" $
    [ testCase ("producer flushes after " <> show outcome) $
        withTopic env (Text.pack ("runner-producer-" <> show outcome)) 1 $ \topic _ -> do
          retained <- newIORef Nothing
          reports <- newEmptyMVar
          let props = P.brokersList (teBrokers env) <> P.sendTimeout (C.Timeout 5000) <> P.extraProp "client.id" "raw-producer-holder" <> P.setCallback (P.deliveryCallback (void . tryPutMVar reports))
              cleanup = readIORef retained >>= mapM_ releaseProducer
          flip finally cleanup $ do
            result <- try @HandlerFailure $ P.runProducer props $ \producer -> do
              let alias = PT.KafkaProducer (PT.kpKafkaPtr producer) (PT.kpKafkaConf producer) (PT.kpTopicConf producer)
              writeIORef retained (Just alias)
              when (outcome == Returned) $ do
                dumpTopicConf (PT.kpTopicConf alias) >>= (\values -> M.lookup "message.timeout.ms" values @?= Just "5000")
                dumpKafkaConf (PT.kpKafkaConf alias) >>= (\values -> M.lookup "client.id" values @?= Just "raw-producer-holder")
                metadata <- topicMetadata (PT.kpKafkaPtr alias) (C.Timeout 5000) topic >>= either throwIO pure
                map tmTopicName (kmTopics metadata) @?= [topic]
              P.produceMessage alias (P.ProducerRecord topic (P.SpecifiedPartition 0) (Just "runner-key") (Just "runner-value") mempty) >>= (@?= Nothing)
              finish outcome
            checkResult outcome result
            tryTakeMVar reports >>= \case
              Just (P.DeliverySuccess record offset) -> (P.prTopic record, P.prKey record, P.prValue record, offset) @?= (topic, Just "runner-key", Just "runner-value", C.Offset 0)
              other -> assertFailure ("wrapper did not flush its delivery report: " <> show other)
    | outcome <- [Returned, Refused, Thrown]
    ]
      <> [ testCase (label <> " consumer closes after " <> show outcome) $
             withTopic env (Text.pack ("runner-consumer-" <> label <> "-" <> show outcome)) 1 $ \topic producer -> do
               produceTo producer topic 0 "runner-record"
               retained <- newIORef Nothing
               let props = C.brokersList (teBrokers env) <> C.groupId (makeGroupId env (Text.pack ("runner-" <> label <> "-" <> show outcome))) <> C.noAutoCommit <> C.clientId (C.ClientId "raw-consumer-holder") <> C.callbackPollMode mode
                   cleanup = readIORef retained >>= mapM_ releaseConsumer
               flip finally cleanup $ do
                 result <- try @HandlerFailure $ C.runConsumer props (C.topics [topic] <> C.offsetReset C.Earliest) $ \consumer -> do
                   let alias = CT.KafkaConsumer (CT.kcKafkaPtr consumer) (CT.kcKafkaConf consumer)
                   writeIORef retained (Just alias)
                   when (outcome == Returned) $ do
                     dumpKafkaConf (CT.kcKafkaConf alias) >>= (\values -> M.lookup "client.id" values @?= Just "raw-consumer-holder")
                     metadata <- topicMetadata (CT.kcKafkaPtr alias) (C.Timeout 5000) topic >>= either throwIO pure
                     map tmTopicName (kmTopics metadata) @?= [topic]
                   C.consumerFatalError alias >>= (@?= Nothing)
                   message <- C.pollMessage alias (C.Timeout 10000) >>= either throwIO pure
                   (C.crTopic message, C.crPartition message, C.crOffset message, C.crValue message) @?= (topic, C.PartitionId 0, C.Offset 0, Just "runner-record")
                   finish outcome
                 checkResult outcome result
                 readIORef retained >>= maybe (assertFailure "runner did not invoke handler") (\consumer -> C.consumerClosed consumer >>= (@?= True))
         | (label, mode) <- [("async", C.CallbackPollModeAsync), ("sync", C.CallbackPollModeSync)],
           outcome <- [Returned, Refused, Thrown]
         ]
      <> [ testCase "producer preserves native construction failure without invoking handler" $ do
             called <- newIORef False
             result <- P.runProducer (P.brokersList (teBrokers env) <> P.extraProps sasl) (\_ -> writeIORef called True >> pure (Right (42 :: Int)))
             nativeFailure result
             readIORef called >>= (@?= False),
           testCase "producer preserves configuration exceptions without invoking handler" $ do
             called <- newIORef False
             result <- try @C.KafkaError $ P.runProducer (P.extraProp "not.a.kafka.property" "x") (\_ -> writeIORef called True >> pure (Right (42 :: Int)))
             unknownConfiguration result
             readIORef called >>= (@?= False)
         ]
      <> [ testCase (label <> " consumer preserves " <> kind <> " construction failure") $ do
             called <- newIORef False
             let base = C.brokersList (teBrokers env) <> C.groupId (makeGroupId env (Text.pack ("runner-failure-" <> label <> "-" <> kind))) <> C.callbackPollMode mode
             if kind == "native"
               then C.runConsumer (base <> C.extraProps sasl) mempty (\_ -> writeIORef called True >> pure (Right (42 :: Int))) >>= nativeFailure
               else (try @C.KafkaError $ C.runConsumer (base <> C.extraProp "not.a.kafka.property" "x") mempty (\_ -> writeIORef called True >> pure (Right (42 :: Int)))) >>= unknownConfiguration
             readIORef called >>= (@?= False)
         | (label, mode) <- [("async", C.CallbackPollModeAsync), ("sync", C.CallbackPollModeSync)],
           kind <- ["native", "configuration"]
         ]

sasl :: M.Map Text.Text Text.Text
sasl = M.fromList [("security.protocol", "SASL_PLAINTEXT"), ("sasl.mechanisms", "PLAIN")]

nativeFailure :: Either C.KafkaError Int -> IO ()
nativeFailure (Left (C.KafkaError message)) = assertBool (show message) ("sasl.username" `Text.isInfixOf` message)
nativeFailure other = assertFailure (show other)

unknownConfiguration :: Either C.KafkaError (Either C.KafkaError Int) -> IO ()
unknownConfiguration (Left (C.KafkaUnknownConfigurationKey _)) = pure ()
unknownConfiguration other = assertFailure (show other)