packages feed

kafka-client-0.7.0.1: test-lib/Test/Kafka/Managed.hs

{-# LANGUAGE RecordWildCards #-}
module Test.Kafka.Managed
    ( KafkaConfig(..)
    , withManagedKafka
    ) where

import Control.Applicative
import Control.Concurrent  (threadDelay)
import Control.Exception   (IOException, bracket, catch)
import System.Exit         (ExitCode (..))

import qualified Network.Socket as N
import qualified System.IO      as IO
import qualified System.IO.Temp as Temp
import qualified System.Process as P

data KafkaConfig = KafkaConfig {
    kafkaServerPort   :: Int
  , kafkaLogDirectory :: FilePath
  } deriving (Show, Eq)

toProperties :: KafkaConfig -> [(String, String)]
toProperties KafkaConfig{..} =
    [ ("brokerid", "0")
    , ("port", show kafkaServerPort)
    , ("log.dir", kafkaLogDirectory)
    , ("num.partitions", "1")
    , ("enable.zookeeper", "false")
    , ("log.flush.interval", "1")
    , ("log.default.flush.interval.ms", "10")
    , ("log.default.flush.scheduler.interval.ms", "5")
    ]

writeProperties :: [(String, String)] -> IO.Handle -> IO ()
writeProperties entries h = IO.hPutStr h . unlines $ do
    (key, value) <- entries
    return $! key ++ "=" ++ value

withManagedKafka :: KafkaConfig -> IO a -> IO a
withManagedKafka config m =
  Temp.withSystemTempFile "kafka.proeprties" $ \configPath handle -> do
    writeProperties (toProperties config) handle
    IO.hClose handle
    bracket (startServer configPath) stopServer . const $ do
        waitUntilConnectable (kafkaServerPort config)
        m

-- | Keep trying to connect to the given port until the server accepts a
-- connection or we've made more than 10 attempts.
waitUntilConnectable :: Int -> IO ()
waitUntilConnectable port = loop (0 :: Int)
  where
    loop attempts
        -- TODO maxAttempts can probably be a parameter
      | attempts >= 10 = error "Can't connect to Kafka after 10 attempts."
      | otherwise = do
          addrInfo <- head <$> N.getAddrInfo
                  (Just $ N.defaultHints {
                          N.addrFlags = [N.AI_NUMERICSERV]
                        , N.addrFamily = N.AF_INET
                        , N.addrSocketType = N.Stream
                        })
                  (Just "localhost") (Just $ show port)
          socket <- N.socket N.AF_INET N.Stream N.defaultProtocol
          let connect = do
                  N.connect socket (N.addrAddress addrInfo)
                  N.close socket
              retry = do
                  threadDelay (200 * 1000)
                  loop (attempts + 1)
          connect `catchIO` const retry
    catchIO :: IO a -> (IOException -> IO a) -> IO a
    catchIO = catch

-- TODO Better stdout/stderr handling. Could return a ManagedKafka object here
-- that allows access to the logs in case of failure.
startServer :: FilePath -> IO P.ProcessHandle
startServer configPath =
  IO.withFile "/dev/null" IO.WriteMode $ \nullHandle -> do
    (_, _, _, h) <- P.createProcess (p nullHandle)
    return h
  where
    p nullHandle = (P.proc "kafka-server-start.sh" [configPath]) {
        P.std_out = P.UseHandle nullHandle
      }

stopServer :: P.ProcessHandle -> IO ()
stopServer h = do
    P.interruptProcessGroupOf h
    code <- P.waitForProcess h
    case code of
      ExitSuccess -> return ()
      ExitFailure 130 -> return ()  -- Exit code 130 == terminated with Ctrl-C
      ExitFailure exitCode -> putStrLn . unwords $
          ["Failed to stop Kafka server. Exit code", show exitCode]