hats-0.1.0.0: src/Network/Nats/ConnectionManager.hs
{-# LANGUAGE RecordWildCards #-}
-- |
-- Module: Network.Nats.ConnectionManager
-- Copyright: (c) 2016 Patrik Sandahl
-- License: MIT
-- Maintainer: Patrik Sandahl <patrik.sandahl@gmail.com>
-- Stability: experimental
-- Portability: portable
--
-- Configuration and functionality for setting up and maintaining
-- connections towards a NATS messaging server.
module Network.Nats.ConnectionManager
( ConnectionManager
, ManagerSettings (..)
, SockAddr
, startConnectionManager
, stopConnectionManager
, defaultSettings
, randomSelect
, roundRobinSelect
) where
import Control.Concurrent (ThreadId, forkIO, killThread, myThreadId)
import Control.Concurrent.STM ( TVar, atomically, newTVarIO
, readTVarIO, writeTVar
)
import Control.Exception (handle, throwIO, throwTo)
import Network.URI (URI)
import Network.Socket (SockAddr)
import System.Random (randomRIO)
import Network.Nats.Connection ( Connection, Downstream
, Upstream, makeConnection
, clientShutdown, waitForShutdown, sockAddr
)
import Network.Nats.Subscriber (SubscriberMap)
import Network.Nats.Types (NatsException (..))
-- | Resourced aquired by the connection manager. The record is
-- opaque to the user.
data ConnectionManager = ConnectionManager
{ connection :: TVar (Maybe Connection)
, managerThread :: ThreadId
}
-- | Internal runtime context for the connection manager.
data ManagerContext = ManagerContext
{ upstream :: !Upstream
, downstream :: !Downstream
, subscriberMap :: !SubscriberMap
, uris :: ![URI]
, currUriIdx :: !Int
, currConn :: TVar (Maybe Connection)
, callerThread :: !ThreadId
}
-- | A set of parameters to guide the behavior of the connection manager.
-- A default set of parameters can be obtained by calling
-- 'defaultSettings'.
data ManagerSettings = ManagerSettings
{ reconnectionAttempts :: !Int
-- ^ The number of times the connection manager shall try to
-- connect a server before giving up.
, maxWaitTimeMS :: !Int
-- ^ Maximum waiting between a connection is made and a CONNECT
-- message is received from the NATS server. If exceeded the
-- connection is terminated and a new server selection is made.
-- The unit for the time is in milliseconds.
, serverSelect :: ([URI], Int) -> IO (URI, Int)
-- ^ A function to select one of the servers from the
-- server pool. The arguments to the selector is the list of server
-- uris and the current index. The reply is the chosen server and
-- its index.
, connectedTo :: SockAddr -> IO ()
-- ^ Callback to inform that a connection is made to the NATS
-- server, with the 'SockAddr' for the server. This callback is
-- made in the connection manager's thread, and the callback's
-- execution time must be minimized. 'Control.Concurrent.forkIO'
-- if longer execution times are needed.
, disconnectedFrom :: SockAddr -> IO ()
-- ^ Callback to inform that a disconnection to the NATS server
-- has happen. Give the 'SockAddr' for the server. This callback is
-- made in the connection manager's thread, and the callback's
-- execution time must be minimized. 'Control.Concurrent.forkIO'
-- if longer execution times are needed.
}
-- | Start the connection manager.
startConnectionManager :: ManagerSettings
-> Upstream
-> Downstream
-> SubscriberMap
-> [URI]
-> IO ConnectionManager
startConnectionManager settings upstream' downstream'
subscriberMap' uris' = do
-- The transactional 'Connection' is shared between the 'ManagerContext'
-- and the 'ConnectionManager'.
conn <- newTVarIO Nothing
caller <- myThreadId
let context = ManagerContext { upstream = upstream'
, downstream = downstream'
, subscriberMap = subscriberMap'
, uris = uris'
, currUriIdx = -1
, currConn = conn
, callerThread = caller
}
ConnectionManager <$> pure conn
<*> forkIO (managerLoop settings context)
-- | Stop the connection manager, clean up stuff.
stopConnectionManager :: ConnectionManager -> IO ()
stopConnectionManager mgr = do
-- The order when shutting down things is important. First the
-- managerThread must be stopped (so it not tries to create new
-- connections). Then the 'Connection' can be stopped.
killThread $ managerThread mgr
-- The 'Connection' is folded in 'Maybe'.
mapM_ clientShutdown =<< readTVarIO (connection mgr)
-- | Create a default 'ManagerSettings'.
defaultSettings :: ManagerSettings
defaultSettings =
ManagerSettings
{ reconnectionAttempts = 5
, maxWaitTimeMS = 2000
, serverSelect = roundRobinSelect
, connectedTo = const (return ())
, disconnectedFrom = const (return ())
}
-- | Make a random selection of a server.
randomSelect :: ([URI], Int) -> IO (URI, Int)
randomSelect (xs, _) = do
idx <- randomRIO (0, length xs - 1)
return (xs !! idx, idx)
-- | Use round robin to select a server.
roundRobinSelect :: ([URI], Int) -> IO (URI, Int)
roundRobinSelect (xs, currIdx)
| currIdx == length xs - 1 = return (head xs, 0)
| otherwise = return (xs !! (currIdx + 1), currIdx + 1)
-- | Connect to a server and maintain the connection.
managerLoop :: ManagerSettings -> ManagerContext -> IO ()
managerLoop mgr@ManagerSettings {..} ctx@ManagerContext {..} =
exceptionForward callerThread `handle` do
(newIdx, conn) <- tryConnect mgr ctx reconnectionAttempts
atomically $ writeTVar currConn (Just conn)
connectedTo $ sockAddr conn
waitForShutdown conn
atomically $ writeTVar currConn Nothing
disconnectedFrom $ sockAddr conn
managerLoop mgr $ ctx { currUriIdx = newIdx }
-- | Select a server and connect.
tryConnect :: ManagerSettings -> ManagerContext -> Int
-> IO (Int, Connection)
tryConnect _ _ 0 = throwIO ConnectionGiveUpException
tryConnect mgr@ManagerSettings {..} ctx@ManagerContext {..} attempts = do
(uri, uriIdx) <- serverSelect (uris, currUriIdx)
maybe (tryConnect mgr ctx (attempts - 1))
(\conn -> return (uriIdx, conn))
=<< makeConnection (toUS maxWaitTimeMS) uri
upstream downstream subscriberMap
-- | Throw 'NatsException's to the provided thread.
exceptionForward :: ThreadId -> NatsException -> IO ()
exceptionForward = throwTo
toUS :: Int -> Int
toUS n = n * 1000