@ -1,4 +1,6 @@
@@ -1,4 +1,6 @@
{- # LANGUAGE OverloadedStrings # -}
{- # LANGUAGE OverloadedStrings # -}
{- # LANGUAGE QuasiQuotes # -}
{- # LANGUAGE ScopedTypeVariables # -}
module ATrade.Broker.Client (
startBrokerClient ,
@ -14,14 +16,19 @@ import ATrade.Broker.Protocol (BrokerServerRequest (..),
@@ -14,14 +16,19 @@ import ATrade.Broker.Protocol (BrokerServerRequest (..),
BrokerServerResponse ( .. ) ,
ClientIdentity , Notification ,
NotificationSqnum ( NotificationSqnum ) ,
RequestSqnum ,
RequestId ( .. ) ,
getNotificationSqnum ,
getRequestId ,
getResponseRequestId ,
nextSqnum )
import ATrade.Logging ( Message ,
Severity ( Debug , Info , Warning ) ,
logWith )
import ATrade.Types ( ClientSecurityParams ( cspCertificate , cspServerCertificate ) ,
Order , OrderId )
import ATrade.Util ( atomicMapIORef )
import ATrade.Utils.MessagePipe ( MessagePipe , emptyMessagePipe ,
getMessages , push )
import Colog ( LogAction )
import Control.Concurrent ( MVar , ThreadId , forkIO ,
killThread , newEmptyMVar ,
@ -29,12 +36,14 @@ import Control.Concurrent (MVar, ThreadId, forkIO,
@@ -29,12 +36,14 @@ import Control.Concurrent (MVar, ThreadId, forkIO,
threadDelay , tryReadMVar ,
yield )
import Control.Concurrent.BoundedChan ( )
import Control.Concurrent.MVar ( )
import Control.Exception ( SomeException , finally , handle ,
throwIO )
import Control.Monad ( forM_ , when )
import Control.Concurrent.MVar ( tryPutMVar )
import Control.Exception ( SomeException , bracket , catch ,
finally , handle , throwIO )
import Control.Monad ( forM_ , forever , void , when )
import Control.Monad.Loops ( andM , whileM_ )
import Data.Aeson ( decode , encode )
import Data.Attoparsec.Text ( char , decimal , maybeResult ,
parseOnly )
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL
import Data.Int ( Int64 )
@ -43,25 +52,22 @@ import Data.IORef (IORef, atomicModifyIORef',
@@ -43,25 +52,22 @@ import Data.IORef (IORef, atomicModifyIORef',
readIORef , writeIORef )
import qualified Data.List as L
import Data.List.NonEmpty ( )
import Data.Maybe ( isNothing )
import Data.Maybe ( isNothing , mapMaybe )
import qualified Data.Text as T
import Data.Text.Encoding ( decodeUtf8 )
import qualified Data.Text.Encoding as T
import qualified Data.Text.Lazy as TL
import Data.Time ( UTCTime , diffUTCTime ,
getCurrentTime )
import Language.Haskell.Printf
import Network.Socket ( Family ( AF_INET ) ,
SockAddr ( SockAddrInet ) ,
Socket , SocketType ( Stream ) ,
connect , defaultProtocol ,
socket , tupleToHostAddress )
import Network.Socket.ByteString ( recv , sendAll )
import Safe ( lastMay )
import System.Timeout ( timeout )
import System.ZMQ4 ( Context , Event ( In ) ,
Poll ( Sock ) , Req ( Req ) ,
Sub ( Sub ) , Switch ( On ) ,
connect , poll , receive ,
receiveMulti , restrict , send ,
setLinger , setReqCorrelate ,
setReqRelaxed , setTcpKeepAlive ,
setTcpKeepAliveCount ,
setTcpKeepAliveIdle ,
setTcpKeepAliveInterval ,
subscribe , withSocket )
import System.ZMQ4.ZAP ( zapApplyCertificate ,
zapSetServerCertificate )
type NotificationCallback = Notification -> IO ()
@ -72,178 +78,130 @@ data BrokerClientHandle = BrokerClientHandle {
@@ -72,178 +78,130 @@ data BrokerClientHandle = BrokerClientHandle {
submitOrder :: Order -> IO ( Either T . Text () ) ,
cancelOrder :: OrderId -> IO ( Either T . Text () ) ,
getNotifications :: IO ( Either T . Text [ Notification ] ) ,
cmdVar :: MVar ( BrokerServerRequest , MVar BrokerServerResponse ) ,
cmdVar :: MVar ( BrokerServerRequest , MVar BrokerServerResponse , UTCTime ) ,
lastKnownNotificationRef :: IORef NotificationSqnum ,
notificationCallback :: [ NotificationCallback ] ,
notificationThreadId :: ThreadId
}
brokerClientThread :: B . ByteString ->
Context ->
data BrokerClientEvent = IncomingResponse BrokerServerResponse
| IncomingNotification Notification
brokerClientThread :: T . Text ->
T . Text ->
MVar ( BrokerServerRequest , MVar BrokerServerResponse ) ->
MVar ( BrokerServerRequest , MVar BrokerServerResponse , UTCTime ) ->
MVar () ->
MVar () ->
ClientSecurityParams ->
[ NotificationCallback ] ->
LogAction IO Message ->
IO ()
brokerClientThread socketIdentity ctx ep cmd comp killMv secParam s logger = finally brokerClientThread' cleanup
brokerClientThread socketIdentity ep cmd comp killMv notificationCallback s logger = finally brokerClientThread' cleanup
where
log = logWith logger
cleanup = log Info " Broker.Client " " Quitting broker client thread " >> putMVar comp ()
brokerClientThread' = whileM_ ( isNothing <$> tryReadMVar killMv ) $ do
log Debug " Broker.Client " " Starting event loop "
handle ( \ e -> do
log Warning " Broker.Client " $ " Broker client: exception: " <> ( T . pack . show ) ( e :: SomeException ) <> " ; isZMQ: " <> ( T . pack . show ) ( isZMQError e )
if isZMQError e
then do
log Debug " Broker.Client " " Rethrowing exception "
throwIO e
else do
return () ) $ withSocket ctx Req ( \ sock -> do
setLinger ( restrict 0 ) sock
setReqCorrelate True sock
setReqRelaxed True sock
case cspCertificate secParams of
Just clientCert -> zapApplyCertificate clientCert sock
Nothing -> return ()
case cspServerCertificate secParams of
Just serverCert -> zapSetServerCertificate serverCert sock
Nothing -> return ()
brokerClientThread' :: IO ()
brokerClientThread' = do
pendingResp <- newEmptyMVar
pipeRef <- newIORef emptyMessagePipe
case parseHostAndPort ep of
Right ( host , port ) -> forever $ do
clientSocket <- socket AF_INET Stream defaultProtocol
flip catch ( \ ( _ :: SomeException ) -> log Warning " Broker.Client " " Connection error " ) $ forever $ do
connect clientSocket $ SockAddrInet ( fromIntegral port ) host
sendAll clientSocket $ B . snoc ( BL . toStrict $ encode ( RequestSetClientIdentity ( RequestId 0 ) socketIdentity ) ) 0
bracket ( forkIO $ sendThread cmd clientSocket pendingResp ) killThread $ \ _ -> do
isTimeout <- newIORef False
whileM_ ( andM [ isNothing <$> tryReadMVar killMv , not <$> readIORef isTimeout ] ) $ do
maybeRawData <- timeout 1000000 $ recv clientSocket 4096
case maybeRawData of
Just rawData -> do
if B . length rawData > 0
then do
atomicMapIORef pipeRef ( push rawData )
messages <- atomicModifyIORef' pipeRef getMessages
let parsed = mapMaybe decodeEvent messages
mapM_ ( handleMessage pendingResp ) parsed
else writeIORef isTimeout True
Nothing -> do
maybePending <- tryReadMVar pendingResp
case maybePending of
Just ( req , respVar , timestamp ) -> do
now <- getCurrentTime
when ( now ` diffUTCTime ` timestamp > 5.0 ) $ do
log Warning " Broker.Client " $ TL . toStrict $ [ t | Request timeout : %?| ] req
void $ takeMVar pendingResp
putMVar respVar $ ResponseError ( getRequestId req ) " Timeout "
_ -> pure ()
log Debug " Broker.Client " " Recv thread done "
threadDelay 1000000
Left err -> log Warning " Broker.Client " $ " Error: " <> ( T . pack . show ) err
connect sock $ T . unpack ep
log Debug " Broker.Client " " Connected "
isTimeout <- newIORef False
whileM_ ( andM [ isNothing <$> tryReadMVar killMv , not <$> readIORef isTimeout ] ) $ do
( request , resp ) <- takeMVar cmd
send sock [] ( BL . toStrict $ encode request )
incomingMessage <- timeout 5000000 $ receive sock
case incomingMessage of
Just msg -> case decode . BL . fromStrict $ msg of
Just response -> putMVar resp response
Nothing -> putMVar resp ( ResponseError " Unable to decode response " )
Nothing -> do
putMVar resp ( ResponseError " Response timeout " )
writeIORef isTimeout True
threadDelay 1000000 )
isZMQError e = " ZMQError " ` L . isPrefixOf ` show e
sendThread cmdvar sock pendingResp = forever $ do
( req , respVar , timestamp ) <- takeMVar cmdvar
putMVar pendingResp ( req , respVar , timestamp )
let json = encode req
log Debug " Broker.Client " $ T . pack $ " sendThread: sending " <> show json
sendAll sock $ BL . toStrict $ BL . snoc json 0
notificationThread :: ClientIdentity ->
[ NotificationCallback ] ->
Context ->
T . Text ->
IORef RequestSqnum ->
MVar ( BrokerServerRequest , MVar BrokerServerResponse ) ->
MVar () ->
ClientSecurityParams ->
LogAction IO Message ->
IORef NotificationSqnum ->
IO ()
notificationThread clientIdentity callbacks ctx ep idCounter cmdVar killMv secParams logger lastKnownNotificationSqnum = flip finally ( return () ) $ do
whileM_ ( isNothing <$> tryReadMVar killMv ) $
withSocket ctx Sub $ \ sock -> do
setLinger ( restrict 0 ) sock
case cspCertificate secParams of
Just clientCert -> zapApplyCertificate clientCert sock
Nothing -> return ()
case cspServerCertificate secParams of
Just serverCert -> zapSetServerCertificate serverCert sock
Nothing -> return ()
setTcpKeepAlive On sock
setTcpKeepAliveCount ( restrict 5 ) sock
setTcpKeepAliveIdle ( restrict 60 ) sock
setTcpKeepAliveInterval ( restrict 10 ) sock
connect sock $ T . unpack ep
log Debug " Broker.Client " $ " Subscribing: [ " <> clientIdentity <> " ] "
subscribe sock $ T . encodeUtf8 clientIdentity
decodeEvent :: B . ByteString -> Maybe BrokerClientEvent
decodeEvent raw = case decode $ BL . fromStrict raw :: Maybe Notification of
Just notif -> Just $ IncomingNotification notif
Nothing -> case decode $ BL . fromStrict raw :: Maybe BrokerServerResponse of
Just response -> Just $ IncomingResponse response
Nothing -> Nothing
initialSqnum <- requestCurrentSqnum cmdVar idCounter clientIdentity
handleMessage :: MVar ( BrokerServerRequest , MVar BrokerServerResponse , UTCTime ) -> BrokerClientEvent -> IO ()
handleMessage respVar ( IncomingResponse resp ) = do
( req , respVar , _ ) <- takeMVar respVar
if getRequestId req == getResponseRequestId resp
then putMVar respVar resp
else do
log Warning " Broker.Client " $ TL . toStrict $ [ t | Request ID mismatch : %?/%?| ] ( getRequestId req ) ( getResponseRequestId resp )
putMVar respVar ( ResponseError ( getRequestId req ) " Request ID mismatch " )
handleMessage _ ( IncomingNotification notif ) = callNotificationCallbacks notif
log Debug " Broker.Client " $ " Got current sqnum: " <> ( T . pack . show ) initialSqnum
notifSqnumRef <- newIORef initialSqnum
whileM_ ( isNothing <$> tryReadMVar killMv ) $ do
evs <- poll 5000 [ Sock sock [ In ] Nothing ]
if null . L . head $ evs
then do
respVar <- newEmptyMVar
sqnum <- nextId idCounter
notifSqnum <- readIORef notifSqnumRef
putMVar cmdVar ( RequestNotifications sqnum clientIdentity notifSqnum , respVar )
resp <- takeMVar respVar
case resp of
( ResponseNotifications ns ) -> do
forM_ ns $ \ notif -> do
lastSqnum <- readIORef notifSqnumRef
when ( getNotificationSqnum notif >= lastSqnum ) $ do
forM_ callbacks $ \ c -> c notif
atomicWriteIORef notifSqnumRef ( nextSqnum lastSqnum )
( ResponseError msg ) -> log Warning " Broker.Client " $ " ResponseError: " <> msg
_ -> log Warning " Broker.Client " " Unknown error when requesting notifications "
else do
msg <- receiveMulti sock
case msg of
[ _ , payload ] -> case decode ( BL . fromStrict payload ) of
Just notification -> do
currentSqnum <- readIORef notifSqnumRef
when ( getNotificationSqnum notification /= currentSqnum ) $ do
log Warning " Broker.Client " $
" Notification sqnum mismatch: " <> ( T . pack . show ) currentSqnum <> " -> " <> ( T . pack . show ) ( getNotificationSqnum notification )
atomicWriteIORef notifSqnumRef ( nextSqnum $ getNotificationSqnum notification )
forM_ callbacks $ \ c -> c notification
atomicWriteIORef lastKnownNotificationSqnum currentSqnum
_ -> return ()
_ -> return ()
where
log = logWith logger
requestCurrentSqnum cmdVar idCounter clientIdentity = do
respVar <- newEmptyMVar
sqnum <- nextId idCounter
putMVar cmdVar ( RequestCurrentSqnum sqnum clientIdentity , respVar )
resp <- takeMVar respVar
case resp of
( ResponseCurrentSqnum sqnum ) -> return sqnum
( ResponseError msg ) -> do
log Warning " Broker.Client " $ " ResponseError: " <> msg
return ( NotificationSqnum 1 )
_ -> do
log Warning " Broker.Client " " Unknown error when requesting notifications "
return ( NotificationSqnum 1 )
callNotificationCallbacks notif = mapM_ ( \ cb -> cb notif ) notificationCallbacks
parseHostAndPort = parseOnly endpointParser
endpointParser = do
b1 <- decimal
void $ char '.'
b2 <- decimal
void $ char '.'
b3 <- decimal
void $ char '.'
b4 <- decimal
void $ char ':'
port <- decimal
pure ( tupleToHostAddress ( b1 , b2 , b3 , b4 ) , port )
startBrokerClient :: B . ByteString -- ^ Socket Identity
-> Context -- ^ ZeroMQ context
-> T . Text -- ^ Broker endpoing
-> T . Text -- ^ Notification endpoing
startBrokerClient :: T . Text -- ^ Socket Identity
-> T . Text -- ^ Broker endpoint
-> [ NotificationCallback ] -- ^ List of notification callbacks
-> ClientSecurityParams -- ^
-> LogAction IO Message
-> IO BrokerClientHandle
startBrokerClient socketIdentity ctx endpoint notifEndpoint notificationCallbacks secParams logger = do
idCounter <- newIORef 1
startBrokerClient socketIdentity endpoint notificationCallbacks logger = do
compMv <- newEmptyMVar
killMv <- newEmptyMVar
cmdVar <- newEmptyMVar :: IO ( MVar ( BrokerServerRequest , MVar BrokerServerResponse ) )
tid <- forkIO ( brokerClientThread socketIdentity ctx endpoint cmdVar compMv killMv secParams logger )
cmdVar <- newEmptyMVar :: IO ( MVar ( BrokerServerRequest , MVar BrokerServerResponse , UTCTime ) )
idCounter <- newIORef 0
tid <- forkIO ( brokerClientThread socketIdentity endpoint cmdVar compMv killMv notificationCallbacks logger )
notifSqnumRef <- newIORef ( NotificationSqnum 0 )
lastKnownNotification <- newIORef ( NotificationSqnum 0 )
notifThreadId <- forkIO ( notificationThread ( T . decodeUtf8 socketIdentity ) notificationCallbacks ctx notifEndpoint idCounter cmdVar killMv secParams logger
lastKnownNotification )
return BrokerClientHandle {
tid = tid ,
completionMvar = compMv ,
killMvar = killMv ,
submitOrder = bcSubmitOrder ( decodeUtf8 socketIdentity ) idCounter cmdVar ,
cancelOrder = bcCancelOrder ( decodeUtf8 socketIdentity ) idCounter cmdVar ,
getNotifications = bcGetNotifications ( decodeUtf8 socketIdentity ) idCounter notifSqnumRef cmdVar lastKnownNotification ,
submitOrder = bcSubmitOrder socketIdentity idCounter cmdVar logger ,
cancelOrder = bcCancelOrder socketIdentity idCounter cmdVar logger ,
getNotifications = bcGetNotifications socketIdentity idCounter notifSqnumRef cmdVar lastKnownNotification logger ,
cmdVar = cmdVar ,
lastKnownNotificationRef = notifSqnumRef ,
notificationCallback = [] ,
notificationThreadId = notifThreadId
}
notificationCallback = []
}
stopBrokerClient :: BrokerClientHandle -> IO ()
stopBrokerClient handle = do
@ -256,45 +214,84 @@ stopBrokerClient handle = do
@@ -256,45 +214,84 @@ stopBrokerClient handle = do
nextId cnt = atomicModifyIORef' cnt ( \ v -> ( v + 1 , v ) )
bcSubmitOrder :: ClientIdentity -> IORef Int64 -> MVar ( BrokerServerRequest , MVar BrokerServerResponse ) -> Order -> IO ( Either T . Text () )
bcSubmitOrder clientIdentity idCounter cmdVar order = do
bcSubmitOrder ::
ClientIdentity ->
IORef Int64 ->
MVar ( BrokerServerRequest , MVar BrokerServerResponse , UTCTime ) ->
LogAction IO Message ->
Order ->
IO ( Either T . Text () )
bcSubmitOrder clientIdentity idCounter cmdVar logger order = do
respVar <- newEmptyMVar
sqnum <- nextId idCounter
putMVar cmdVar ( RequestSubmitOrder sqnum clientIdentity order , respVar )
resp <- takeMVar respVar
case resp of
ResponseOk -> return $ Right ()
( ResponseError msg ) -> return $ Left msg
_ -> return $ Left " Unknown error "
now <- getCurrentTime
result <- timeout 3000000 $ do
putMVar cmdVar ( RequestSubmitOrder ( RequestId sqnum ) clientIdentity order , respVar , now )
resp <- takeMVar respVar
case resp of
ResponseOk ( RequestId requestId ) -> do
if requestId == sqnum
then return $ Right ()
else do
logWith logger Warning " Broker.Client " " SubmitOrder: requestId mismatch "
pure $ Left " requestid mismatch "
( ResponseError ( RequestId _ ) msg ) -> return $ Left msg
_ -> return $ Left " Unknown error "
case result of
Just r -> pure r
_ -> pure $ Left " Request timeout "
bcCancelOrder :: ClientIdentity -> IORef RequestSqnum -> MVar ( BrokerServerRequest , MVar BrokerServerResponse ) -> OrderId -> IO ( Either T . Text () )
bcCancelOrder clientIdentity idCounter cmdVar orderId = do
bcCancelOrder ::
ClientIdentity ->
IORef Int64 ->
MVar ( BrokerServerRequest , MVar BrokerServerResponse , UTCTime ) ->
LogAction IO Message ->
OrderId ->
IO ( Either T . Text () )
bcCancelOrder clientIdentity idCounter cmdVar logger orderId = do
respVar <- newEmptyMVar
sqnum <- nextId idCounter
putMVar cmdVar ( RequestCancelOrder sqnum clientIdentity orderId , respVar )
resp <- takeMVar respVar
case resp of
ResponseOk -> return $ Right ()
( ResponseError msg ) -> return $ Left msg
_ -> return $ Left " Unknown error "
now <- getCurrentTime
result <- timeout 3000000 $ do
putMVar cmdVar ( RequestCancelOrder ( RequestId sqnum ) clientIdentity orderId , respVar , now )
resp <- takeMVar respVar
case resp of
ResponseOk ( RequestId requestId ) -> do
if requestId == sqnum
then return $ Right ()
else do
logWith logger Warning " Broker.Client " " CancelOrder: requestId mismatch "
pure $ Left " requestid mismatch "
( ResponseError ( RequestId _ ) msg ) -> return $ Left msg
_ -> return $ Left " Unknown error "
case result of
Just r -> pure $ r
_ -> pure $ Left " Request timeout "
bcGetNotifications :: ClientIdentity ->
IORef RequestSqnum ->
IORef Int64 ->
IORef NotificationSqnum ->
MVar ( BrokerServerRequest , MVar BrokerServerResponse ) ->
MVar ( BrokerServerRequest , MVar BrokerServerResponse , UTCTime ) ->
IORef NotificationSqnum ->
LogAction IO Message ->
IO ( Either T . Text [ Notification ] )
bcGetNotifications clientIdentity idCounter notifSqnumRef cmdVar lastKnownNotification = do
bcGetNotifications clientIdentity idCounter notifSqnumRef cmdVar lastKnownNotification logger = do
respVar <- newEmptyMVar
sqnum <- nextId idCounter
notifSqnum <- nextSqnum <$> readIORef notifSqnumRef
putMVar cmdVar ( RequestNotifications sqnum clientIdentity notifSqnum , respVar )
now <- getCurrentTime
putMVar cmdVar ( RequestNotifications ( RequestId sqnum ) clientIdentity notifSqnum , respVar , now )
resp <- takeMVar respVar
case resp of
( ResponseNotifications ns ) -> do
case lastMay ns of
Just n -> atomicWriteIORef notifSqnumRef ( getNotificationSqnum n )
Nothing -> readIORef lastKnownNotification >>= atomicWriteIORef notifSqnumRef
return $ Right ns
( ResponseError msg ) -> return $ Left msg
( ResponseNotifications ( RequestId requestId ) ns ) ->
if ( requestId == sqnum )
then do
case lastMay ns of
Just n -> atomicWriteIORef notifSqnumRef ( getNotificationSqnum n )
Nothing -> readIORef lastKnownNotification >>= atomicWriteIORef notifSqnumRef
return $ Right ns
else do
logWith logger Warning " Broker.Client " " GetNotifications: requestId mismatch "
return $ Left " requestId mismatch "
( ResponseError ( RequestId requestId ) msg ) -> return $ Left msg
_ -> return $ Left " Unknown error "