2018-12-13 10:26:15 +03:00
|
|
|
{-# LANGUAGE RankNTypes #-}
|
2020-03-05 20:59:26 +03:00
|
|
|
{-# LANGUAGE NondecreasingIndentation #-}
|
2018-07-20 10:22:46 +03:00
|
|
|
|
|
|
|
module Hasura.GraphQL.Transport.WebSocket.Server
|
|
|
|
( WSId(..)
|
|
|
|
|
|
|
|
, WSConn
|
|
|
|
, getData
|
|
|
|
, getWSId
|
|
|
|
, closeConn
|
|
|
|
, sendMsg
|
|
|
|
|
2019-05-14 09:24:46 +03:00
|
|
|
, AcceptWith(..)
|
2018-07-20 10:22:46 +03:00
|
|
|
, OnConnH
|
|
|
|
, OnCloseH
|
|
|
|
, OnMessageH
|
|
|
|
, WSHandlers(..)
|
|
|
|
|
|
|
|
, WSServer
|
2020-01-07 23:25:32 +03:00
|
|
|
, WSEventInfo(..)
|
|
|
|
, WSQueueResponse(..)
|
2018-07-20 10:22:46 +03:00
|
|
|
, createWSServer
|
|
|
|
, closeAll
|
|
|
|
, createServerApp
|
2019-09-09 23:26:04 +03:00
|
|
|
, shutdown
|
2018-07-20 10:22:46 +03:00
|
|
|
) where
|
|
|
|
|
2019-12-11 04:04:49 +03:00
|
|
|
import qualified Control.Concurrent.Async as A
|
|
|
|
import qualified Control.Concurrent.Async.Lifted.Safe as LA
|
|
|
|
import qualified Control.Concurrent.STM as STM
|
2020-03-05 20:59:26 +03:00
|
|
|
import Control.Exception.Lifted
|
2019-12-11 04:04:49 +03:00
|
|
|
import qualified Control.Monad.Trans.Control as MC
|
|
|
|
import qualified Data.Aeson as J
|
|
|
|
import qualified Data.Aeson.Casing as J
|
|
|
|
import qualified Data.Aeson.TH as J
|
|
|
|
import qualified Data.ByteString.Lazy as BL
|
2020-03-05 20:59:26 +03:00
|
|
|
import Data.String
|
2019-12-11 04:04:49 +03:00
|
|
|
import qualified Data.TByteString as TBS
|
|
|
|
import qualified Data.UUID as UUID
|
|
|
|
import qualified Data.UUID.V4 as UUID
|
2020-01-07 23:25:32 +03:00
|
|
|
import Data.Word (Word16)
|
|
|
|
import GHC.Int (Int64)
|
|
|
|
import Hasura.Prelude
|
2018-07-20 10:22:46 +03:00
|
|
|
import qualified ListT
|
2019-12-11 04:04:49 +03:00
|
|
|
import qualified Network.WebSockets as WS
|
|
|
|
import qualified StmContainers.Map as STMMap
|
2018-07-20 10:22:46 +03:00
|
|
|
|
2019-12-11 04:04:49 +03:00
|
|
|
import qualified Hasura.Logging as L
|
2018-07-20 10:22:46 +03:00
|
|
|
|
|
|
|
newtype WSId
|
|
|
|
= WSId { unWSId :: UUID.UUID }
|
|
|
|
deriving (Show, Eq, Hashable)
|
|
|
|
|
|
|
|
instance J.ToJSON WSId where
|
|
|
|
toJSON (WSId uuid) =
|
|
|
|
J.toJSON $ UUID.toText uuid
|
|
|
|
|
|
|
|
data WSEvent
|
|
|
|
= EConnectionRequest
|
|
|
|
| EAccepted
|
|
|
|
| ERejected
|
|
|
|
| EMessageReceived !TBS.TByteString
|
|
|
|
| EMessageSent !TBS.TByteString
|
2019-05-14 09:24:46 +03:00
|
|
|
| EJwtExpired
|
2018-07-20 10:22:46 +03:00
|
|
|
| ECloseReceived
|
|
|
|
| ECloseSent !TBS.TByteString
|
|
|
|
| EClosed
|
|
|
|
deriving (Show, Eq)
|
|
|
|
$(J.deriveToJSON
|
|
|
|
J.defaultOptions { J.constructorTagModifier = J.snakeCase . drop 1
|
|
|
|
, J.sumEncoding = J.TaggedObject "type" "detail"
|
|
|
|
}
|
|
|
|
''WSEvent)
|
|
|
|
|
2020-01-07 23:25:32 +03:00
|
|
|
-- extra websocket event info
|
|
|
|
data WSEventInfo
|
|
|
|
= WSEventInfo
|
|
|
|
{ _wseiQueryExecutionTime :: !(Maybe Double)
|
|
|
|
, _wseiResponseSize :: !(Maybe Int64)
|
|
|
|
} deriving (Show, Eq)
|
|
|
|
$(J.deriveToJSON
|
|
|
|
J.defaultOptions { J.fieldLabelModifier = J.snakeCase . drop 5
|
|
|
|
, J.omitNothingFields = True
|
|
|
|
}
|
|
|
|
''WSEventInfo)
|
|
|
|
|
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
data WSLog
|
|
|
|
= WSLog
|
|
|
|
{ _wslWebsocketId :: !WSId
|
|
|
|
, _wslEvent :: !WSEvent
|
2020-01-07 23:25:32 +03:00
|
|
|
, _wslMetadata :: !(Maybe WSEventInfo)
|
2018-07-20 10:22:46 +03:00
|
|
|
} deriving (Show, Eq)
|
2020-01-07 23:25:32 +03:00
|
|
|
$(J.deriveToJSON
|
|
|
|
J.defaultOptions { J.fieldLabelModifier = J.snakeCase . drop 4
|
|
|
|
, J.omitNothingFields = True
|
|
|
|
}
|
|
|
|
''WSLog)
|
2018-07-20 10:22:46 +03:00
|
|
|
|
2019-11-26 15:14:21 +03:00
|
|
|
instance L.ToEngineLog WSLog L.Hasura where
|
2018-07-20 10:22:46 +03:00
|
|
|
toEngineLog wsLog =
|
2019-11-26 15:14:21 +03:00
|
|
|
(L.LevelDebug, L.ELTInternal L.ILTWsServer, J.toJSON wsLog)
|
2018-07-20 10:22:46 +03:00
|
|
|
|
2020-01-07 23:25:32 +03:00
|
|
|
data WSQueueResponse
|
|
|
|
= WSQueueResponse
|
|
|
|
{ _wsqrMessage :: !BL.ByteString
|
|
|
|
, _wsqrEventInfo :: !(Maybe WSEventInfo)
|
|
|
|
-- ^ extra metadata that we use for other actions, such as print log
|
|
|
|
-- we don't want to inlcude them into websocket message payload
|
|
|
|
}
|
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
data WSConn a
|
|
|
|
= WSConn
|
|
|
|
{ _wcConnId :: !WSId
|
2019-11-26 15:14:21 +03:00
|
|
|
, _wcLogger :: !(L.Logger L.Hasura)
|
2018-07-20 10:22:46 +03:00
|
|
|
, _wcConnRaw :: !WS.Connection
|
2020-01-07 23:25:32 +03:00
|
|
|
, _wcSendQ :: !(STM.TQueue WSQueueResponse)
|
2018-07-20 10:22:46 +03:00
|
|
|
, _wcExtraData :: !a
|
|
|
|
}
|
|
|
|
|
|
|
|
getData :: WSConn a -> a
|
|
|
|
getData = _wcExtraData
|
|
|
|
|
|
|
|
getWSId :: WSConn a -> WSId
|
|
|
|
getWSId = _wcConnId
|
|
|
|
|
|
|
|
closeConn :: WSConn a -> BL.ByteString -> IO ()
|
2020-01-07 23:25:32 +03:00
|
|
|
closeConn wsConn = closeConnWithCode wsConn 1000 -- 1000 is "normal close"
|
2019-09-09 23:26:04 +03:00
|
|
|
|
|
|
|
-- | Closes a connection with code 1012, which means "Server is restarting"
|
|
|
|
-- good clients will implement a retry logic with a backoff of a few seconds
|
2019-12-11 04:04:49 +03:00
|
|
|
forceConnReconnect :: MonadIO m => WSConn a -> BL.ByteString -> m ()
|
|
|
|
forceConnReconnect wsConn bs = liftIO $ closeConnWithCode wsConn 1012 bs
|
2019-09-09 23:26:04 +03:00
|
|
|
|
|
|
|
closeConnWithCode :: WSConn a -> Word16 -> BL.ByteString -> IO ()
|
|
|
|
closeConnWithCode wsConn code bs = do
|
2020-01-07 23:25:32 +03:00
|
|
|
(L.unLogger . _wcLogger) wsConn $
|
|
|
|
WSLog (_wcConnId wsConn) (ECloseSent $ TBS.fromLBS bs) Nothing
|
2019-09-09 23:26:04 +03:00
|
|
|
WS.sendCloseCode (_wcConnRaw wsConn) code bs
|
2018-07-20 10:22:46 +03:00
|
|
|
|
|
|
|
-- writes to a queue instead of the raw connection
|
|
|
|
-- so that sendMsg doesn't block
|
2020-01-07 23:25:32 +03:00
|
|
|
sendMsg :: WSConn a -> WSQueueResponse -> IO ()
|
|
|
|
sendMsg wsConn = STM.atomically . STM.writeTQueue (_wcSendQ wsConn)
|
2018-07-20 10:22:46 +03:00
|
|
|
|
|
|
|
type ConnMap a = STMMap.Map WSId (WSConn a)
|
|
|
|
|
2019-09-09 23:26:04 +03:00
|
|
|
data ServerStatus a
|
|
|
|
= AcceptingConns !(ConnMap a)
|
|
|
|
| ShuttingDown
|
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
data WSServer a
|
|
|
|
= WSServer
|
2019-11-26 15:14:21 +03:00
|
|
|
{ _wssLogger :: !(L.Logger L.Hasura)
|
2019-09-09 23:26:04 +03:00
|
|
|
, _wssStatus :: !(STM.TVar (ServerStatus a))
|
2020-03-05 20:59:26 +03:00
|
|
|
-- ^ See e.g. createServerApp.onAccept for how we use STM to preserve consistency
|
2018-07-20 10:22:46 +03:00
|
|
|
}
|
|
|
|
|
2019-11-26 15:14:21 +03:00
|
|
|
createWSServer :: L.Logger L.Hasura -> STM.STM (WSServer a)
|
2019-09-09 23:26:04 +03:00
|
|
|
createWSServer logger = do
|
|
|
|
connMap <- STMMap.new
|
|
|
|
serverStatus <- STM.newTVar (AcceptingConns connMap)
|
|
|
|
return $ WSServer logger serverStatus
|
2018-07-20 10:22:46 +03:00
|
|
|
|
|
|
|
closeAll :: WSServer a -> BL.ByteString -> IO ()
|
2019-09-09 23:26:04 +03:00
|
|
|
closeAll (WSServer (L.Logger writeLog) serverStatus) msg = do
|
2018-07-20 10:22:46 +03:00
|
|
|
writeLog $ L.debugT "closing all connections"
|
2019-09-09 23:26:04 +03:00
|
|
|
conns <- STM.atomically $ flushConnMap serverStatus
|
|
|
|
closeAllWith (flip closeConn) msg conns
|
|
|
|
|
|
|
|
closeAllWith
|
|
|
|
:: (BL.ByteString -> WSConn a -> IO ())
|
|
|
|
-> BL.ByteString
|
|
|
|
-> [(WSId, WSConn a)]
|
|
|
|
-> IO ()
|
|
|
|
closeAllWith closer msg conns =
|
|
|
|
void $ A.mapConcurrently (closer msg . snd) conns
|
|
|
|
|
|
|
|
-- | Resets the current connections map to an empty one if the server is
|
|
|
|
-- running and returns the list of connections that were in the map
|
|
|
|
-- before flushing it.
|
|
|
|
flushConnMap :: STM.TVar (ServerStatus a) -> STM.STM [(WSId, WSConn a)]
|
|
|
|
flushConnMap serverStatus = do
|
|
|
|
status <- STM.readTVar serverStatus
|
|
|
|
case status of
|
|
|
|
AcceptingConns connMap -> do
|
|
|
|
conns <- ListT.toList $ STMMap.listT connMap
|
|
|
|
STMMap.reset connMap
|
|
|
|
return conns
|
|
|
|
ShuttingDown -> return []
|
2018-07-20 10:22:46 +03:00
|
|
|
|
2019-05-14 09:24:46 +03:00
|
|
|
data AcceptWith a
|
|
|
|
= AcceptWith
|
|
|
|
{ _awData :: !a
|
|
|
|
, _awReq :: !WS.AcceptRequest
|
|
|
|
, _awKeepAlive :: !(Maybe (WSConn a -> IO ()))
|
|
|
|
, _awOnJwtExpiry :: !(Maybe (WSConn a -> IO ()))
|
|
|
|
}
|
|
|
|
|
2019-12-11 04:04:49 +03:00
|
|
|
type OnConnH m a = WSId -> WS.RequestHead -> m (Either WS.RejectRequest (AcceptWith a))
|
|
|
|
type OnCloseH m a = WSConn a -> m ()
|
|
|
|
type OnMessageH m a = WSConn a -> BL.ByteString -> m ()
|
2018-07-20 10:22:46 +03:00
|
|
|
|
2019-12-11 04:04:49 +03:00
|
|
|
data WSHandlers m a
|
2018-07-20 10:22:46 +03:00
|
|
|
= WSHandlers
|
2019-12-11 04:04:49 +03:00
|
|
|
{ _hOnConn :: OnConnH m a
|
|
|
|
, _hOnMessage :: OnMessageH m a
|
|
|
|
, _hOnClose :: OnCloseH m a
|
2018-07-20 10:22:46 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
createServerApp
|
2019-12-11 04:04:49 +03:00
|
|
|
:: (MonadIO m, MC.MonadBaseControl IO m, LA.Forall (LA.Pure m))
|
|
|
|
=> WSServer a
|
2018-07-20 10:22:46 +03:00
|
|
|
-- user provided handlers
|
2019-12-11 04:04:49 +03:00
|
|
|
-> WSHandlers m a
|
2018-07-20 10:22:46 +03:00
|
|
|
-- aka WS.ServerApp
|
2019-09-09 23:26:04 +03:00
|
|
|
-> WS.PendingConnection
|
2019-12-11 04:04:49 +03:00
|
|
|
-> m ()
|
2019-09-09 23:26:04 +03:00
|
|
|
createServerApp (WSServer logger@(L.Logger writeLog) serverStatus) wsHandlers pendingConn = do
|
2019-12-11 04:04:49 +03:00
|
|
|
wsId <- WSId <$> liftIO UUID.nextRandom
|
2020-01-07 23:25:32 +03:00
|
|
|
writeLog $ WSLog wsId EConnectionRequest Nothing
|
2019-12-11 04:04:49 +03:00
|
|
|
status <- liftIO $ STM.readTVarIO serverStatus
|
2019-09-09 23:26:04 +03:00
|
|
|
case status of
|
2020-03-05 20:59:26 +03:00
|
|
|
AcceptingConns _ -> logUnexpectedExceptions $ do
|
2019-09-09 23:26:04 +03:00
|
|
|
let reqHead = WS.pendingRequest pendingConn
|
|
|
|
onConnRes <- _hOnConn wsHandlers wsId reqHead
|
|
|
|
either (onReject wsId) (onAccept wsId) onConnRes
|
|
|
|
|
|
|
|
ShuttingDown ->
|
|
|
|
onReject wsId shuttingDownReject
|
2018-07-20 10:22:46 +03:00
|
|
|
|
|
|
|
where
|
2020-03-05 20:59:26 +03:00
|
|
|
-- It's not clear what the unexpected exception handling story here should be. So at
|
|
|
|
-- least log properly and re-raise:
|
|
|
|
logUnexpectedExceptions = handle $ \(e :: SomeException) -> do
|
|
|
|
writeLog $ L.UnstructuredLog L.LevelError $ fromString $
|
|
|
|
"Unexpected exception raised in websocket. Please report this as a bug: "<>show e
|
|
|
|
throwIO e
|
|
|
|
|
2019-09-09 23:26:04 +03:00
|
|
|
shuttingDownReject =
|
|
|
|
WS.RejectRequest 503
|
|
|
|
"Service Unavailable"
|
|
|
|
[("Retry-After", "0")]
|
|
|
|
"Server is shutting down"
|
|
|
|
|
2018-07-20 10:22:46 +03:00
|
|
|
onReject wsId rejectRequest = do
|
2019-12-11 04:04:49 +03:00
|
|
|
liftIO $ WS.rejectRequestWith pendingConn rejectRequest
|
2020-01-07 23:25:32 +03:00
|
|
|
writeLog $ WSLog wsId ERejected Nothing
|
2018-07-20 10:22:46 +03:00
|
|
|
|
2019-05-14 09:24:46 +03:00
|
|
|
onAccept wsId (AcceptWith a acceptWithParams keepAliveM onJwtExpiryM) = do
|
2019-12-11 04:04:49 +03:00
|
|
|
conn <- liftIO $ WS.acceptRequestWith pendingConn acceptWithParams
|
2020-01-07 23:25:32 +03:00
|
|
|
writeLog $ WSLog wsId EAccepted Nothing
|
2019-12-11 04:04:49 +03:00
|
|
|
sendQ <- liftIO STM.newTQueueIO
|
2018-09-27 14:22:49 +03:00
|
|
|
let wsConn = WSConn wsId logger conn sendQ a
|
2019-09-09 23:26:04 +03:00
|
|
|
|
2020-03-05 20:59:26 +03:00
|
|
|
let whenAcceptingInsertConn = liftIO $ STM.atomically $ do
|
|
|
|
status <- STM.readTVar serverStatus
|
|
|
|
case status of
|
|
|
|
ShuttingDown -> pure ()
|
|
|
|
AcceptingConns connMap -> STMMap.insert wsConn wsId connMap
|
|
|
|
return status
|
|
|
|
|
|
|
|
-- ensure we clean up connMap even if an unexpected exception is raised from our worker
|
|
|
|
-- threads, or an async exception is raised somewhere in the body here:
|
|
|
|
bracket
|
|
|
|
whenAcceptingInsertConn
|
|
|
|
(onConnClose wsConn)
|
|
|
|
$ \case
|
2019-09-09 23:26:04 +03:00
|
|
|
ShuttingDown -> do
|
|
|
|
-- Bad luck, we were in the process of shutting the server down but a new
|
|
|
|
-- connection was accepted. Let's just close it politely
|
|
|
|
forceConnReconnect wsConn "shutting server down"
|
|
|
|
_hOnClose wsHandlers wsConn
|
|
|
|
|
2020-03-05 20:59:26 +03:00
|
|
|
AcceptingConns _ -> do
|
|
|
|
let rcv = forever $ do
|
|
|
|
-- Process all messages serially (important!), in a separate thread:
|
|
|
|
msg <- liftIO $ WS.receiveData conn
|
|
|
|
writeLog $ WSLog wsId (EMessageReceived $ TBS.fromLBS msg) Nothing
|
|
|
|
_hOnMessage wsHandlers wsConn msg
|
|
|
|
|
|
|
|
let send = forever $ do
|
|
|
|
WSQueueResponse msg wsInfo <- liftIO $ STM.atomically $ STM.readTQueue sendQ
|
|
|
|
liftIO $ WS.sendTextData conn msg
|
|
|
|
writeLog $ WSLog wsId (EMessageSent $ TBS.fromLBS msg) wsInfo
|
|
|
|
|
|
|
|
let withAsyncM mAction cont = case mAction of
|
|
|
|
Nothing -> cont Nothing
|
|
|
|
Just action -> LA.withAsync (liftIO $ action wsConn) $
|
|
|
|
\actRef -> cont $ Just actRef
|
|
|
|
|
|
|
|
-- withAsync lets us be very sure that if e.g. an async exception is raised while we're
|
|
|
|
-- forking that the threads we launched will be cleaned up. See also below.
|
|
|
|
LA.withAsync rcv $ \rcvRef -> do
|
|
|
|
LA.withAsync send $ \sendRef -> do
|
|
|
|
withAsyncM keepAliveM $ \keepAliveRefM -> do
|
|
|
|
withAsyncM onJwtExpiryM $ \onJwtExpiryRefM -> do
|
2019-09-09 23:26:04 +03:00
|
|
|
|
|
|
|
-- terminates on WS.ConnectionException and JWT expiry
|
|
|
|
let waitOnRefs = catMaybes [keepAliveRefM, onJwtExpiryRefM]
|
|
|
|
<> [rcvRef, sendRef]
|
2020-03-05 20:59:26 +03:00
|
|
|
-- withAnyCancel re-raises exceptions from forkedThreads, and is guarenteed to cancel in
|
|
|
|
-- case of async exceptions raised while blocking here:
|
|
|
|
try (LA.waitAnyCancel waitOnRefs) >>= \case
|
2019-09-09 23:26:04 +03:00
|
|
|
Left ( _ :: WS.ConnectionException) -> do
|
2020-01-07 23:25:32 +03:00
|
|
|
writeLog $ WSLog (_wcConnId wsConn) ECloseReceived Nothing
|
2019-09-09 23:26:04 +03:00
|
|
|
-- this will happen when jwt is expired
|
|
|
|
Right _ -> do
|
2020-01-07 23:25:32 +03:00
|
|
|
writeLog $ WSLog (_wcConnId wsConn) EJwtExpired Nothing
|
2019-09-09 23:26:04 +03:00
|
|
|
|
2020-03-05 20:59:26 +03:00
|
|
|
onConnClose wsConn = \case
|
|
|
|
ShuttingDown -> pure ()
|
|
|
|
AcceptingConns connMap -> do
|
|
|
|
liftIO $ STM.atomically $ STMMap.delete (_wcConnId wsConn) connMap
|
|
|
|
_hOnClose wsHandlers wsConn
|
|
|
|
writeLog $ WSLog (_wcConnId wsConn) EClosed Nothing
|
2019-09-09 23:26:04 +03:00
|
|
|
|
|
|
|
|
|
|
|
shutdown :: WSServer a -> IO ()
|
|
|
|
shutdown (WSServer (L.Logger writeLog) serverStatus) = do
|
|
|
|
writeLog $ L.debugT "Shutting websockets server down"
|
|
|
|
conns <- STM.atomically $ do
|
|
|
|
conns <- flushConnMap serverStatus
|
|
|
|
STM.writeTVar serverStatus ShuttingDown
|
|
|
|
return conns
|
|
|
|
closeAllWith (flip forceConnReconnect) "shutting server down" conns
|