packages feed

supernova-0.0.1: src/Pulsar/Connection.hs

{-# LANGUAGE GeneralizedNewtypeDeriving, LambdaCase, OverloadedStrings #-}

module Pulsar.Connection where

import           Control.Monad                  ( forever )
import           Control.Monad.Catch            ( MonadThrow )
import           Control.Monad.Managed
import qualified Data.Binary                   as B
import           Data.Foldable                  ( traverse_ )
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.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 )
import           UnliftIO.Async                 ( concurrently_ )
import           UnliftIO.Chan
import           UnliftIO.Concurrent            ( forkIO
                                                , killThread
                                                , threadDelay
                                                )
import           UnliftIO.Exception             ( bracket
                                                , throwIO
                                                )

newtype Connection = Conn NS.Socket

newtype ReqId = ReqId B.Word64 deriving (Num, Show)
newtype SeqId = SeqId B.Word64 deriving (Num, Show)
newtype ProducerId = PId B.Word64 deriving (Num, Show)
newtype ConsumerId = CId B.Word64 deriving (Num, Show)

data AppState = AppState
  { appConsumers :: [(ConsumerId, Chan Response)] -- a list of consumer identifiers associated with a communication channel
  , appConsumerId :: ConsumerId                   -- an incremental counter to assign unique consumer ids
  , appProducers :: [(ProducerId, Chan Response)] -- a list of producer identifiers associated with a communication channel
  , appProducerId :: ProducerId                   -- an incremental counter to assign unique producer ids
  , appRequestId :: ReqId                         -- an incremental counter to assign unique request ids for all commands
  }

mkConsumerId :: MonadIO m => Chan Response -> IORef AppState -> m ConsumerId
mkConsumerId chan ref = liftIO $ atomicModifyIORef
  ref
  (\(AppState cs cid ps pid rid) ->
    let cid' = cid + 1 in (AppState ((cid', chan) : cs) cid' ps pid rid, cid)
  )

mkProducerId :: MonadIO m => Chan Response -> IORef AppState -> m ProducerId
mkProducerId chan ref = liftIO $ atomicModifyIORef
  ref
  (\(AppState cs cid ps pid rid) ->
    let pid' = pid + 1 in (AppState cs cid ((pid', chan) : ps) pid' rid, pid)
  )

mkRequestId :: MonadIO m => IORef AppState -> m ReqId
mkRequestId ref = liftIO $ atomicModifyIORef
  ref
  (\(AppState cs cid ps pid req) ->
    let req' = req + 1 in (AppState cs cid ps pid req', req)
  )

{- | 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
  }

{- | 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
  :: (MonadThrow m, MonadIO m, MonadManaged m) => ConnectData -> m PulsarCtx
connect (ConnData h p) = do
  socket <- acquireSocket h p
  liftIO $ sendSimpleCmd socket P.connect
  resp <- receive socket
  case getCommand resp ^. F.maybe'connected of
    Just _  -> logResponse resp
    Nothing -> throwIO $ userError "Could not connect"
  app   <- liftIO initAppState
  kchan <- liftIO newChan
  let ctx        = Ctx (Conn socket) app
      dispatcher = recvDispatch socket app kchan
      task       = concurrently_ dispatcher (keepAlive socket kchan)
  using $ ctx <$ managed (bracket (forkIO task) killThread)

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

recvDispatch
  :: MonadIO m => NS.Socket -> IORef AppState -> Chan BaseCommand -> m ()
recvDispatch s ref chan = forever $ do
  resp                   <- receive s
  (AppState cs _ ps _ _) <- liftIO $ readIORef ref
  case getCommand resp ^. F.maybe'pong of
    Just _  -> writeChan chan (getCommand resp)
    Nothing -> traverse_ (`writeChan` resp) ((snd <$> cs) ++ (snd <$> ps))

{- Emit a PING and expect a PONG every 29 seconds. If a PONG is not received, interrupt connection -}
keepAlive :: MonadIO m => NS.Socket -> Chan BaseCommand -> m ()
keepAlive s chan = forever $ do
  threadDelay (29 * 1000000)
  logRequest P.ping
  sendSimpleCmd s P.ping
  liftIO $ 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