packages feed

supernova-0.0.3: src/Pulsar/Connection.hs

{-# LANGUAGE LambdaCase, OverloadedStrings #-}

module Pulsar.Connection where

import           Control.Applicative            ( (<|>) )
import           Control.Concurrent             ( forkIO
                                                , killThread
                                                , threadDelay
                                                )
import           Control.Concurrent.Async       ( async
                                                , concurrently_
                                                )
import           Control.Concurrent.Chan
import           Control.Concurrent.MVar
import           Control.Exception              ( throwIO )
import           Control.Monad                  ( forever
                                                , when
                                                )
import           Control.Monad.Catch            ( MonadThrow
                                                , bracket
                                                )
import           Control.Monad.IO.Class
import           Control.Monad.Managed          ( MonadManaged
                                                , managed
                                                , runManaged
                                                )
import           Data.Foldable                  ( traverse_ )
import           Data.Functor                   ( void )
import           Data.IORef
import           Lens.Family
import qualified Network.Socket                as NS
import qualified Network.Socket.ByteString.Lazy
                                               as SBL
import           Proto.PulsarApi                ( BaseCommand
                                                , MessageMetadata
                                                )
import qualified Proto.PulsarApi_Fields        as F
import           Pulsar.AppState
import           Pulsar.Internal.Logger
import           Pulsar.Internal.TCPClient      ( acquireSocket )
import qualified Pulsar.Protocol.Commands      as P
import           Pulsar.Protocol.Decoder        ( decodeBaseCommand )
import           Pulsar.Protocol.Encoder        ( encodeBaseCommand )
import           Pulsar.Protocol.Frame          ( Payload
                                                , Response(..)
                                                , frameMaxSize
                                                , getCommand
                                                )
import           System.Timeout                 ( timeout )

newtype Connection = Conn NS.Socket

{- | Connection details: host and port. -}
data ConnectData = ConnData
    { connHost :: NS.HostName
    , connPort :: NS.ServiceName
    } deriving Show

{- | Internal Pulsar context. You will never need to access its content (not exported) but might need to take it as argument. -}
data PulsarCtx = Ctx
  { ctxConn :: Connection
  , ctxState :: IORef AppState
  , ctxConnWorker :: Worker
  }

{- | Default connection data: "127.0.0.1:6650" -}
defaultConnectData :: ConnectData
defaultConnectData = ConnData { connHost = "127.0.0.1", connPort = "6650" }

{- | Starts a Pulsar connection with the supplied 'ConnectData' -}
connect
  :: (MonadIO m, MonadThrow m, MonadManaged m) => ConnectData -> m PulsarCtx
connect (ConnData h p) = do
  socket <- acquireSocket h p
  liftIO $ sendSimpleCmd socket P.connect
  checkConnection socket
  app   <- liftIO initAppState
  kchan <- liftIO newChan
  var   <- liftIO newEmptyMVar
  let
    dispatcher = recvDispatch socket app kchan
    task       = concurrently_ dispatcher (keepAlive socket kchan)
    handler =
      managed (bracket (forkIO task) (\i -> readMVar var >> killThread i))
  worker <- liftIO $ async (runManaged $ void handler)
  return $ Ctx (Conn socket) app (worker, var)

checkConnection :: (MonadIO m, MonadThrow m) => NS.Socket -> m ()
checkConnection socket = do
  resp <- receive socket
  case getCommand resp ^. F.maybe'connected of
    Just _  -> logResponse resp
    Nothing -> liftIO . throwIO $ userError "Could not connect"

initAppState :: MonadIO m => m (IORef AppState)
initAppState = liftIO . newIORef $ AppState [] 0 0 0 [] [] []

responseForRequest :: BaseCommand -> Maybe ReqId
responseForRequest cmd =
  let cmd1 = view F.requestId <$> cmd ^. F.maybe'success
      cmd2 = view F.requestId <$> cmd ^. F.maybe'producerSuccess
      cmd3 = view F.requestId <$> cmd ^. F.maybe'lookupTopicResponse
  in  ReqId <$> (cmd1 <|> cmd2 <|> cmd3)

responseForSendReceipt :: BaseCommand -> Maybe (ProducerId, SeqId)
responseForSendReceipt cmd =
  let cmd' = cmd ^. F.maybe'sendReceipt
      pid  = PId . view F.producerId <$> cmd'
      sid  = SeqId . view F.sequenceId <$> cmd'
  in  (,) <$> pid <*> sid

pongResponse :: BaseCommand -> Chan BaseCommand -> IO (Maybe ())
pongResponse cmd chan =
  traverse (const $ writeChan chan cmd) (cmd ^. F.maybe'pong)

messageResponse :: BaseCommand -> Maybe ConsumerId
messageResponse cmd =
  let cmd' = cmd ^. F.maybe'message
      cid  = view F.consumerId <$> cmd'
  in  CId <$> cid

{- | It listens to incoming messages directly from the network socket and it writes them to all the
 - consumers and producers' communication channels. -}
recvDispatch :: NS.Socket -> IORef AppState -> Chan BaseCommand -> IO ()
recvDispatch s ref chan = forever $ do
  resp <- receive s
  cs   <- _appConsumers <$> readIORef ref
  let
    f = \rid -> registerReqResponse ref rid resp
    g = (\(pid, sid) -> registerSendReceipt ref pid sid resp)
    h = \cid ->
      traverse (\(cid', cn) -> when (cid == cid') (writeChan cn resp)) cs
    cmd = getCommand resp
  traverse_ f (responseForRequest cmd)
  traverse_ g (responseForSendReceipt cmd)
  traverse_ h (messageResponse cmd)
  pongResponse cmd chan

{- Emit a PING and expect a PONG every 29 seconds. If a PONG is not received, interrupt connection -}
keepAlive :: NS.Socket -> Chan BaseCommand -> IO ()
keepAlive s chan = forever $ do
  threadDelay (29 * 1000000)
  logRequest P.ping
  sendSimpleCmd s P.ping
  timeout (2 * 1000000) (readChan chan) >>= \case
    Just cmd -> logResponse cmd
    Nothing  -> throwIO $ userError "Keep Alive interruption"

sendSimpleCmd :: MonadIO m => NS.Socket -> BaseCommand -> m ()
sendSimpleCmd s cmd =
  liftIO . SBL.sendAll s $ encodeBaseCommand Nothing Nothing cmd

sendPayloadCmd
  :: MonadIO m
  => NS.Socket
  -> BaseCommand
  -> MessageMetadata
  -> Maybe Payload
  -> m ()
sendPayloadCmd s cmd meta payload =
  liftIO . SBL.sendAll s $ encodeBaseCommand (Just meta) payload cmd

receive :: MonadIO m => NS.Socket -> m Response
receive s = liftIO $ do
  msg <- SBL.recv s $ fromIntegral frameMaxSize
  case decodeBaseCommand msg of
    Left  e    -> fail $ "Decoding error: " <> e
    Right resp -> case getCommand resp ^. F.maybe'ping of
      Just _ -> do
        logResponse $ getCommand resp
        logRequest P.pong
        sendSimpleCmd s P.pong
        return resp
      Nothing -> return resp