Browse Source

Broker.Server: check request sequence numbers

master
Denis Tereshkin 9 years ago
parent
commit
bb69bcda3b
  1. 1
      libatrade.cabal
  2. 8
      src/ATrade/Broker/Protocol.hs
  3. 42
      src/ATrade/Broker/Server.hs
  4. 58
      test/MockBroker.hs
  5. 94
      test/TestBrokerServer.hs

1
libatrade.cabal

@ -74,6 +74,7 @@ test-suite libatrade-test
ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wincomplete-patterns ghc-options: -threaded -rtsopts -with-rtsopts=-N -Wincomplete-patterns
default-language: Haskell2010 default-language: Haskell2010
other-modules: ArbitraryInstances other-modules: ArbitraryInstances
, MockBroker
, TestBrokerProtocol , TestBrokerProtocol
, TestBrokerServer , TestBrokerServer
, TestQuoteSourceServer , TestQuoteSourceServer

8
src/ATrade/Broker/Protocol.hs

@ -5,7 +5,8 @@ module ATrade.Broker.Protocol (
BrokerServerResponse(..), BrokerServerResponse(..),
Notification(..), Notification(..),
notificationOrderId, notificationOrderId,
RequestSqnum(..) RequestSqnum(..),
requestSqnum
) where ) where
import qualified Data.HashMap.Strict as HM import qualified Data.HashMap.Strict as HM
@ -22,6 +23,11 @@ data BrokerServerRequest = RequestSubmitOrder RequestSqnum Order
| RequestNotifications RequestSqnum | RequestNotifications RequestSqnum
deriving (Eq, Show) deriving (Eq, Show)
requestSqnum :: BrokerServerRequest -> RequestSqnum
requestSqnum (RequestSubmitOrder sqnum _) = sqnum
requestSqnum (RequestCancelOrder sqnum _) = sqnum
requestSqnum (RequestNotifications sqnum) = sqnum
instance FromJSON BrokerServerRequest where instance FromJSON BrokerServerRequest where
parseJSON = withObject "object" (\obj -> do parseJSON = withObject "object" (\obj -> do
sqnum <- obj .: "request-sqnum" sqnum <- obj .: "request-sqnum"

42
src/ATrade/Broker/Server.hs

@ -39,7 +39,7 @@ data BrokerServerState = BrokerServerState {
bsSocket :: Socket Router, bsSocket :: Socket Router,
orderToBroker :: M.Map OrderId BrokerInterface, orderToBroker :: M.Map OrderId BrokerInterface,
orderMap :: M.Map OrderId PeerId, -- Matches 0mq client identities with corresponding orders orderMap :: M.Map OrderId PeerId, -- Matches 0mq client identities with corresponding orders
lastPacket :: M.Map PeerId (RequestSqnum, B.ByteString), lastPacket :: M.Map PeerId (RequestSqnum, BrokerServerResponse),
pendingNotifications :: M.Map PeerId [Notification], pendingNotifications :: M.Map PeerId [Notification],
brokers :: [BrokerInterface], brokers :: [BrokerInterface],
completionMvar :: MVar (), completionMvar :: MVar (),
@ -87,20 +87,45 @@ brokerServerThread state = finally brokerServerThread' cleanup
sock <- bsSocket <$> readIORef state sock <- bsSocket <$> readIORef state
msg <- receiveMulti sock msg <- receiveMulti sock
case msg of case msg of
[peerId, _, payload] -> handleMessage peerId payload >>= sendMessage sock peerId [peerId, _, payload] ->
case decode . BL.fromStrict $ payload of
Just request -> do
let sqnum = requestSqnum request
-- Here, we should check if previous packet sequence number is the same
-- If it is, we should resend previous response
lastPackMap <- lastPacket <$> readIORef state
case shouldResend sqnum peerId lastPackMap of
Just response -> sendMessage sock peerId response -- Resend
Nothing -> do
-- Handle incoming request, send response
response <- handleMessage peerId request
sendMessage sock peerId response
-- and store response in case we'll need to resend it
atomicMapIORef state (\s -> s { lastPacket = M.insert peerId (sqnum, response) (lastPacket s)})
Nothing -> do
-- If we weren't able to parse request, we should send error
-- but shouldn't update lastPacket
let response = ResponseError "Invalid request"
sendMessage sock peerId response
_ -> warningM "Broker.Server" ("Invalid packet received: " ++ show msg) _ -> warningM "Broker.Server" ("Invalid packet received: " ++ show msg)
shouldResend sqnum peerId lastPackMap = case M.lookup peerId lastPackMap of
Just (lastSqnum, response) -> if sqnum == lastSqnum
then Just response
else Nothing
Nothing -> Nothing
cleanup = do cleanup = do
sock <- bsSocket <$> readIORef state sock <- bsSocket <$> readIORef state
close sock close sock
mv <- completionMvar <$> readIORef state mv <- completionMvar <$> readIORef state
putMVar mv () putMVar mv ()
handleMessage :: B.ByteString -> B.ByteString -> IO BrokerServerResponse handleMessage :: PeerId -> BrokerServerRequest -> IO BrokerServerResponse
handleMessage peerId payload = do handleMessage peerId request = do
bros <- brokers <$> readIORef state bros <- brokers <$> readIORef state
case decode . BL.fromStrict $ payload of case request of
Just (RequestSubmitOrder sqnum order) -> RequestSubmitOrder sqnum order ->
case findBrokerForAccount (orderAccountId order) bros of case findBrokerForAccount (orderAccountId order) bros of
Just bro -> do Just bro -> do
oid <- nextOrderId oid <- nextOrderId
@ -111,21 +136,20 @@ brokerServerThread state = finally brokerServerThread' cleanup
return $ ResponseOrderSubmitted oid return $ ResponseOrderSubmitted oid
Nothing -> return $ ResponseError "Unknown account" Nothing -> return $ ResponseError "Unknown account"
Just (RequestCancelOrder sqnum oid) -> do RequestCancelOrder sqnum oid -> do
m <- orderToBroker <$> readIORef state m <- orderToBroker <$> readIORef state
case M.lookup oid m of case M.lookup oid m of
Just bro -> do Just bro -> do
cancelOrder bro oid cancelOrder bro oid
return $ ResponseOrderCancelled oid return $ ResponseOrderCancelled oid
Nothing -> return $ ResponseError "Unknown order" Nothing -> return $ ResponseError "Unknown order"
Just (RequestNotifications sqnum) -> do RequestNotifications sqnum -> do
maybeNs <- M.lookup peerId . pendingNotifications <$> readIORef state maybeNs <- M.lookup peerId . pendingNotifications <$> readIORef state
case maybeNs of case maybeNs of
Just ns -> do Just ns -> do
atomicMapIORef state (\s -> s { pendingNotifications = M.insert peerId [] (pendingNotifications s)}) atomicMapIORef state (\s -> s { pendingNotifications = M.insert peerId [] (pendingNotifications s)})
return $ ResponseNotifications ns return $ ResponseNotifications ns
Nothing -> return $ ResponseNotifications [] Nothing -> return $ ResponseNotifications []
Nothing -> return $ ResponseError "Unable to parse request"
sendMessage sock peerId resp = sendMulti sock (peerId :| [B.empty, BL.toStrict . encode $ resp]) sendMessage sock peerId resp = sendMulti sock (peerId :| [B.empty, BL.toStrict . encode $ resp])

58
test/MockBroker.hs

@ -0,0 +1,58 @@
module MockBroker (
MockBrokerState(..),
mockSubmitOrder,
mockCancelOrder,
mockStopBroker,
mkMockBroker
) where
import ATrade.Types
import ATrade.Broker.Protocol
import ATrade.Broker.Server
import ATrade.Util
import Data.IORef
import qualified Data.List as L
data MockBrokerState = MockBrokerState {
orders :: [Order],
cancelledOrders :: [Order],
notificationCallback :: Maybe (Notification -> IO ())
}
mockSubmitOrder :: IORef MockBrokerState -> Order -> IO ()
mockSubmitOrder state order = do
atomicMapIORef state (\s -> s { orders = submittedOrder : orders s })
maybeCb <- notificationCallback <$> readIORef state
case maybeCb of
Just cb -> cb $ OrderNotification (orderId order) Submitted
Nothing -> return ()
where
submittedOrder = order { orderState = Submitted }
mockCancelOrder :: IORef MockBrokerState -> OrderId -> IO Bool
mockCancelOrder state oid = do
ors <- orders <$> readIORef state
case L.find (\o -> orderId o == oid) ors of
Just order -> atomicModifyIORef' state (\s -> (s { cancelledOrders = order : cancelledOrders s}, True))
Nothing -> return False
mockStopBroker :: IORef MockBrokerState -> IO ()
mockStopBroker state = return ()
mkMockBroker accs = do
state <- newIORef MockBrokerState {
orders = [],
cancelledOrders = [],
notificationCallback = Nothing
}
return (BrokerInterface {
accounts = accs,
setNotificationCallback = \cb -> atomicMapIORef state (\s -> s { notificationCallback = cb }),
submitOrder = mockSubmitOrder state,
cancelOrder = mockCancelOrder state,
stopBroker = mockStopBroker state
}, state)

94
test/TestBrokerServer.hs

@ -31,49 +31,7 @@ import qualified Data.List as L
import Data.IORef import Data.IORef
import Data.UUID as U import Data.UUID as U
import Data.UUID.V4 as UV4 import Data.UUID.V4 as UV4
import MockBroker
data MockBrokerState = MockBrokerState {
orders :: [Order],
cancelledOrders :: [Order],
notificationCallback :: Maybe (Notification -> IO ())
}
mockSubmitOrder :: IORef MockBrokerState -> Order -> IO ()
mockSubmitOrder state order = do
atomicMapIORef state (\s -> s { orders = submittedOrder : orders s })
maybeCb <- notificationCallback <$> readIORef state
case maybeCb of
Just cb -> cb $ OrderNotification (orderId order) Submitted
Nothing -> return ()
where
submittedOrder = order { orderState = Submitted }
mockCancelOrder :: IORef MockBrokerState -> OrderId -> IO Bool
mockCancelOrder state oid = do
ors <- orders <$> readIORef state
case L.find (\o -> orderId o == oid) ors of
Just order -> atomicModifyIORef' state (\s -> (s { cancelledOrders = order : cancelledOrders s}, True))
Nothing -> return False
mockStopBroker :: IORef MockBrokerState -> IO ()
mockStopBroker state = return ()
mkMockBroker accs = do
state <- newIORef MockBrokerState {
orders = [],
cancelledOrders = [],
notificationCallback = Nothing
}
return (BrokerInterface {
accounts = accs,
setNotificationCallback = \cb -> atomicMapIORef state (\s -> s { notificationCallback = cb }),
submitOrder = mockSubmitOrder state,
cancelOrder = mockCancelOrder state,
stopBroker = mockStopBroker state
}, state)
unitTests = testGroup "Broker.Server" [testBrokerServerStartStop unitTests = testGroup "Broker.Server" [testBrokerServerStartStop
, testBrokerServerSubmitOrder , testBrokerServerSubmitOrder
@ -81,12 +39,12 @@ unitTests = testGroup "Broker.Server" [testBrokerServerStartStop
, testBrokerServerCancelOrder , testBrokerServerCancelOrder
, testBrokerServerCancelUnknownOrder , testBrokerServerCancelUnknownOrder
, testBrokerServerCorruptedPacket , testBrokerServerCorruptedPacket
, testBrokerServerGetNotifications ] , testBrokerServerGetNotifications
, testBrokerServerDuplicateRequest ]
testBrokerServerStartStop = testCase "Broker Server starts and stops" $ withContext (\ctx -> do --
ep <- toText <$> UV4.nextRandom -- Few helpers
broS <- startBrokerServer [] ctx ("inproc://brokerserver" `T.append` ep) --
stopBrokerServer broS)
makeEndpoint = do makeEndpoint = do
uid <- toText <$> UV4.nextRandom uid <- toText <$> UV4.nextRandom
@ -108,6 +66,14 @@ defaultOrder = mkOrder {
orderOperation = Buy orderOperation = Buy
} }
--
-- Tests
--
testBrokerServerStartStop = testCase "Broker Server starts and stops" $ withContext (\ctx -> do
ep <- toText <$> UV4.nextRandom
broS <- startBrokerServer [] ctx ("inproc://brokerserver" `T.append` ep)
stopBrokerServer broS)
testBrokerServerSubmitOrder = testCaseSteps "Broker Server submits order" $ \step -> withContext (\ctx -> do testBrokerServerSubmitOrder = testCaseSteps "Broker Server submits order" $ \step -> withContext (\ctx -> do
step "Setup" step "Setup"
@ -252,6 +218,10 @@ testBrokerServerGetNotifications = testCaseSteps "Broker Server: notifications r
send sock [] (BL.toStrict . encode $ RequestNotifications 2) send sock [] (BL.toStrict . encode $ RequestNotifications 2)
threadDelay 10000 threadDelay 10000
-- We should obtain 3 notifications:
-- 1. When order became Submitted (from Unsubmitted)
-- 2. When order became Executed (our first notificationCallback call)
-- 3. Corresponding Trade notificatiot (our second notificationCallback call)
step "Reading response" step "Reading response"
resp <- decode . BL.fromStrict <$> receive sock resp <- decode . BL.fromStrict <$> receive sock
case resp of case resp of
@ -277,3 +247,31 @@ testBrokerServerGetNotifications = testCaseSteps "Broker Server: notifications r
Just _ -> assertFailure "Invalid response" Just _ -> assertFailure "Invalid response"
Nothing -> assertFailure "Invalid response" Nothing -> assertFailure "Invalid response"
))) )))
testBrokerServerDuplicateRequest = testCaseSteps "Broker Server: duplicate request" $ \step -> withContext (\ctx -> do
step "Setup"
(mockBroker, broState) <- mkMockBroker ["demo"]
ep <- makeEndpoint
bracket (startBrokerServer [mockBroker] ctx ep) stopBrokerServer (\broS ->
withSocket ctx Req (\sock -> do
connectAndSendOrder step sock defaultOrder ep
step "Reading response"
(Just (ResponseOrderSubmitted orderId)) <- decode . BL.fromStrict <$> receive sock
step "Sending duplicate request (with same sequence number)"
send sock [] (BL.toStrict . encode $ RequestSubmitOrder 1 defaultOrder)
threadDelay 10000
step "Checking that only one order is submitted"
s <- readIORef broState
(length . orders) s @?= 1
step "Reading response from duplicate request"
resp <- decode . BL.fromStrict <$> receive sock
case resp of
Just (ResponseOrderSubmitted oid) -> orderId @?= oid
Just _ -> assertFailure "Invalid response"
Nothing -> assertFailure "Invalid response"
)))

Loading…
Cancel
Save