packages feed

natskell-0.3.0.0: internal/Policy/State/Store.hs

{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}

module State.Store
  ( ClientState
  , newClientState
  , runClient
  , config
  , queue
  , connection
  , enqueue
  , openQueue
  , closeQueue
  , pushPingAction
  , runNextPingAction
  , nextSid
  , nextInbox
  , readServerInfo
  , setServerInfo
  , updateLogContextFromInfo
  , setConnectName
  , setEndpoint
  , readStatus
  , setClosing
  , markClosed
  , waitForClosed
  , waitForNotRunning
  , waitForServerInfo
  , markConnected
  , markConnectionReady
  , readConnectionGeneration
  , waitForConnectionGenerationAfter
  , readAttemptIndex
  , incrementAttemptIndex
  ) where

import           Control.Concurrent.STM
import           Control.Monad          (unless)
import qualified Data.ByteString        as BS
import qualified Data.ByteString.Char8  as BC
import           Data.Maybe             (fromMaybe)
import           Lib.Logger
    ( LogLevel (..)
    , MonadLogger (..)
    , loggerApi
    )
import           Lib.Logger.Types       (AppM, LogContext (..))
import           Lib.LoggerAPI          (runWithLogger, updateLogContext)
import           Network.ConnectionAPI  (Conn)
import           Nuid                   (Nuid, newNuidIO, nextNuid)
import           Queue.API              (Queue (Queue), QueueItem, close, open)
import           Sid                    (SIDCounter, initialSIDCounter, nextSID)
import           State.Types
import           Types.Info             (Info, client_id)
import           Types.Msg              (SID, Subject)

data ClientState = ClientState
                     { clientConfig               :: ClientConfig
                     , clientQueue                :: Queue
                     , clientPings                :: TQueue (IO ())
                     , clientConnectedOnce        :: TVar Bool
                     , clientConnectionGeneration :: TVar Int
                     , clientSidCounter           :: TVar SIDCounter
                     , clientInboxNuid            :: TVar Nuid
                     , clientServerInfo           :: TVar (Maybe Info)
                     , clientAttemptIndex         :: TVar Int
                     , clientStatus               :: TVar ClientStatus
                     , clientConnection           :: Conn
                     , clientLogContext           :: TVar LogContext
                     }

newClientState :: ClientConfig -> Queue -> Conn -> TVar LogContext -> IO ClientState
newClientState cfg queue conn logContext = do
  pings <- newTQueueIO
  connectedOnce <- newTVarIO False
  connectionGeneration <- newTVarIO 0
  sidCounter <- newTVarIO initialSIDCounter
  inboxNuid <- newTVarIO =<< newNuidIO
  serverInfo <- newTVarIO Nothing
  attemptIndex <- newTVarIO 0
  status <- newTVarIO Running
  pure ClientState
    { clientConfig = cfg
    , clientQueue = queue
    , clientPings = pings
    , clientConnectedOnce = connectedOnce
    , clientConnectionGeneration = connectionGeneration
    , clientSidCounter = sidCounter
    , clientInboxNuid = inboxNuid
    , clientServerInfo = serverInfo
    , clientAttemptIndex = attemptIndex
    , clientStatus = status
    , clientConnection = conn
    , clientLogContext = logContext
    }

runClient :: ClientState -> AppM a -> IO a
runClient client =
  runWithLogger loggerApi (loggerConfig (clientConfig client)) (clientLogContext client)

config :: ClientState -> ClientConfig
config = clientConfig

queue :: ClientState -> Queue
queue = clientQueue

connection :: ClientState -> Conn
connection = clientConnection

enqueue :: ClientState -> QueueItem -> IO ()
enqueue client item = do
  let Queue queueEnqueue _ _ _ = clientQueue client
  result <- queueEnqueue item
  case result of
    Left err ->
      runClient client $
        logMessage Error ("enqueueing item failed: " ++ err)
    Right () ->
      pure ()

openQueue :: ClientState -> IO ()
openQueue = open . clientQueue

closeQueue :: ClientState -> IO ()
closeQueue = close . clientQueue

pushPingAction :: ClientState -> IO () -> IO ()
pushPingAction client action =
  atomically $ writeTQueue (clientPings client) action

runNextPingAction :: ClientState -> IO ()
runNextPingAction client = do
  action <- atomically $ tryReadTQueue (clientPings client)
  fromMaybe (pure ()) action

nextSid :: ClientState -> IO SID
nextSid client =
  atomically $ do
    counter <- readTVar (clientSidCounter client)
    let (sid, counter') = nextSID counter
    writeTVar (clientSidCounter client) counter'
    pure sid

nextInbox :: ClientState -> IO Subject
nextInbox client =
  atomically $ do
    nuid <- readTVar (clientInboxNuid client)
    let (token, nextNuidValue) = nextNuid nuid
    writeTVar (clientInboxNuid client) nextNuidValue
    pure ("_INBOX." <> token)

readServerInfo :: ClientState -> IO (Maybe Info)
readServerInfo = readTVarIO . clientServerInfo

setServerInfo :: ClientState -> Info -> IO ()
setServerInfo client info =
  atomically $ writeTVar (clientServerInfo client) (Just info)

updateLogContextFromInfo :: ClientState -> Info -> IO ()
updateLogContextFromInfo client info =
  updateLogContext loggerApi (clientLogContext client) $ \ctx ->
    ctx { lcClientId = client_id info }

setConnectName :: ClientState -> Maybe BS.ByteString -> IO ()
setConnectName client maybeName =
  updateLogContext loggerApi (clientLogContext client) $ \ctx ->
    ctx { lcConnectName = BC.unpack <$> maybeName }

setEndpoint :: ClientState -> (String, Int) -> IO ()
setEndpoint client (host, port) =
  updateLogContext loggerApi (clientLogContext client) $ \ctx ->
    ctx { lcServer = Just (host ++ ":" ++ show port) }

readStatus :: ClientState -> IO ClientStatus
readStatus = readTVarIO . clientStatus

setClosing :: ClientState -> ClientExitReason -> IO ()
setClosing client reason =
  atomically . modifyTVar' (clientStatus client) $ \case
    Closed result  -> Closed result
    Closing result -> Closing result
    Running        -> Closing reason

markClosed :: ClientState -> ClientExitReason -> IO (Maybe ClientExitReason)
markClosed client fallbackReason =
  atomically $ do
    status <- readTVar (clientStatus client)
    case status of
      Closed _ ->
        pure Nothing
      Closing reason -> do
        writeTVar (clientStatus client) (Closed reason)
        pure (Just reason)
      Running -> do
        writeTVar (clientStatus client) (Closed fallbackReason)
        pure (Just fallbackReason)

waitForClosed :: ClientState -> STM ()
waitForClosed client = do
  status <- readTVar (clientStatus client)
  case status of
    Closed _ -> pure ()
    _        -> retry

waitForNotRunning :: ClientState -> STM ()
waitForNotRunning client = do
  status <- readTVar (clientStatus client)
  case status of
    Running -> retry
    _       -> pure ()

waitForServerInfo :: ClientState -> STM ()
waitForServerInfo client = do
  info <- readTVar (clientServerInfo client)
  case info of
    Just _  -> pure ()
    Nothing -> retry

markConnected :: ClientState -> IO Bool
markConnected client =
  atomically $ do
    alreadyConnected <- readTVar (clientConnectedOnce client)
    writeTVar (clientConnectedOnce client) True
    pure alreadyConnected

markConnectionReady :: ClientState -> IO ()
markConnectionReady client =
  atomically $
    modifyTVar' (clientConnectionGeneration client) (+ 1)

readConnectionGeneration :: ClientState -> IO Int
readConnectionGeneration =
  readTVarIO . clientConnectionGeneration

waitForConnectionGenerationAfter :: ClientState -> Int -> STM ()
waitForConnectionGenerationAfter client generation = do
  current <- readTVar (clientConnectionGeneration client)
  unless (current > generation) retry

readAttemptIndex :: ClientState -> IO Int
readAttemptIndex = readTVarIO . clientAttemptIndex

incrementAttemptIndex :: ClientState -> IO ()
incrementAttemptIndex client =
  atomically (modifyTVar' (clientAttemptIndex client) (+ 1))