packages feed

natskell-0.3.0.0: internal/Plumbing/Network/Connection/Core.hs

{-# LANGUAGE TypeApplications #-}

module Network.Connection.Core
  ( ReadError
  , WriteError
  , Transport (..)
  , TransportOption (..)
  , Conn
  , newConn
  , readData
  , closeReader
  , openReader
  , writeData
  , writeDataLazy
  , closeWriter
  , openWriter
  , openConn
  , closeConn
  , pointTransport
  , currentTransport
  , bufferRead
  , enableReadWorker
  ) where

import           Control.Concurrent       (forkIO)
import           Control.Concurrent.STM
import           Control.Exception
import           Control.Monad
import           Data.ByteString          (ByteString)
import qualified Data.ByteString          as BS
import qualified Data.ByteString.Lazy     as LBS
import           Data.Maybe               (isJust, isNothing)
import           Network.Connection.Types

newConn :: IO Conn
newConn =
  Conn
    <$> newEmptyTMVarIO
    <*> newEmptyTMVarIO
    <*> newEmptyTMVarIO
    <*> newTVarIO mempty
    <*> newTBQueueIO 1000
    <*> newTVarIO False
    <*> newTVarIO False

readChunkSize :: Int
readChunkSize = 4096

startReadWorker :: Conn -> IO ()
startReadWorker conn = do
  shouldStart <- atomically $ do
    enabled <- readTVar (readWorkerEnabled conn)
    running <- readTVar (readWorkerRunning conn)
    if not enabled || running
      then return False
      else do
        writeTVar (readWorkerRunning conn) True
        return True
  when shouldStart . void $ forkIO (readWorkerLoop conn)

readWorkerLoop :: Conn -> IO ()
readWorkerLoop conn = do
  let cleanup = atomically $ writeTVar (readWorkerRunning conn) False
  finally loop cleanup
  where
    loop = do
      (blocked, enabled) <- atomically $ do
        blocked <- tryReadTMVar (readBlock conn)
        enabled <- readTVar (readWorkerEnabled conn)
        return (blocked, enabled)
      case (blocked, enabled) of
        (Just _, _) -> return ()
        (_, False)  -> return ()
        _ -> do
          current <- currentTransport conn
          case current of
            Nothing -> return ()
            Just currentTransport' -> do
              result <- try @SomeException (transportRead currentTransport' readChunkSize)
              shouldContinue <- enqueueReadResult conn result
              case result of
                Left _  -> return ()
                Right bytes
                  | BS.null bytes -> return ()
                  | otherwise     -> when shouldContinue loop

enqueueReadResult :: Conn -> Either SomeException ByteString -> IO Bool
enqueueReadResult conn result = atomically $
  (do
      blocked <- tryReadTMVar (readBlock conn)
      check (isNothing blocked)
      writeTBQueue (readQueue conn) (either (Left . show) Right result)
      return True)
  `orElse`
  (do
      _ <- readTMVar (readBlock conn)
      return False)

enableReadWorker :: Conn -> IO ()
enableReadWorker conn = atomically $ writeTVar (readWorkerEnabled conn) True

readData :: Conn -> Int -> IO (Either ReadError ByteString)
readData conn n = do
  blocked <- atomically $ tryReadTMVar (readBlock conn)
  if isJust blocked
    then return $ Left "Read operation is blocked"
    else do
      buffered <- atomically $ do
        buf <- readTVar (readBuffer conn)
        if BS.null buf
          then return Nothing
          else do
            let (chunk, rest) = BS.splitAt n buf
            writeTVar (readBuffer conn) rest
            return (Just chunk)
      case buffered of
        Just chunk -> return $ Right chunk
        Nothing -> do
          current <- currentTransport conn
          case current of
            Nothing -> return $ Left "Transport not initialized"
            Just currentTransport' -> do
              enabled <- readTVarIO (readWorkerEnabled conn)
              if enabled
                then do
                  startReadWorker conn
                  result <- atomically $
                    (Left "Read operation is blocked" <$ readTMVar (readBlock conn))
                    `orElse`
                    readTBQueue (readQueue conn)
                  case result of
                    Left err -> return $ Left err
                    Right bytes -> do
                      let (chunk, rest) = BS.splitAt n bytes
                      unless (BS.null rest)
                        (atomically $ modifyTVar' (readBuffer conn) (<> rest))
                      return $ Right chunk
                else do
                  resultVar <- newEmptyTMVarIO
                  _ <- forkIO $ do
                    result <- try @SomeException (transportRead currentTransport' n)
                    atomically . void $ tryPutTMVar resultVar result
                  result <- atomically $
                    (Left "Read operation is blocked" <$ readTMVar (readBlock conn))
                    `orElse`
                    (Right <$> readTMVar resultVar)
                  case result of
                    Left err -> return $ Left err
                    Right (Left err) -> return $ Left (show err)
                    Right (Right bytes) -> do
                      let (chunk, rest) = BS.splitAt n bytes
                      unless (BS.null rest)
                        (atomically $ modifyTVar' (readBuffer conn) (<> rest))
                      return $ Right chunk

closeReader :: Conn -> IO ()
closeReader conn = void . atomically $ tryPutTMVar (readBlock conn) ()

openReader :: Conn -> IO ()
openReader conn = void . atomically $ tryTakeTMVar (readBlock conn)

writeData :: Conn -> ByteString -> IO (Either WriteError ())
writeData conn bytes = do
  blocked <- atomically $ tryReadTMVar (writeBlock conn)
  if isJust blocked
    then return $ Left "Write operation is blocked"
    else do
      current <- currentTransport conn
      case current of
        Nothing -> return $ Left "Transport not initialized"
        Just currentTransport' -> do
          result <- try @SomeException $ do
            transportWrite currentTransport' bytes
            transportFlush currentTransport'
          case result of
            Left err -> return $ Left (show err)
            Right _  -> return $ Right ()

writeDataLazy :: Conn -> LBS.ByteString -> IO (Either WriteError ())
writeDataLazy conn bytes = do
  blocked <- atomically $ tryReadTMVar (writeBlock conn)
  if isJust blocked
    then return $ Left "Write operation is blocked"
    else do
      current <- currentTransport conn
      case current of
        Nothing -> return $ Left "Transport not initialized"
        Just currentTransport' -> do
          result <- try @SomeException $ do
            transportWriteLazy currentTransport' bytes
            transportFlush currentTransport'
          case result of
            Left err -> return $ Left (show err)
            Right _  -> return $ Right ()

closeWriter :: Conn -> IO ()
closeWriter conn = void . atomically $ tryPutTMVar (writeBlock conn) ()

openWriter :: Conn -> IO ()
openWriter conn = void . atomically $ tryTakeTMVar (writeBlock conn)

pointTransport :: Conn -> Transport -> IO ()
pointTransport conn newTransport = atomically $ do
  _ <- tryTakeTMVar (transport conn)
  putTMVar (transport conn) newTransport

currentTransport :: Conn -> IO (Maybe Transport)
currentTransport conn = atomically $ tryReadTMVar (transport conn)

closeConn :: Conn -> IO ()
closeConn conn = do
  closeReader conn
  closeWriter conn
  current <- currentTransport conn
  case current of
    Nothing -> return ()
    Just currentTransport' -> void $ try @SomeException (transportClose currentTransport')
  waitForReadWorkerStopped conn

openConn :: Conn -> IO ()
openConn conn = do
  openReader conn
  openWriter conn
  atomically $ do
    writeTVar (readBuffer conn) mempty
    void $ flushTBQueue (readQueue conn)
    writeTVar (readWorkerEnabled conn) False

bufferRead :: Conn -> ByteString -> IO ()
bufferRead conn bytes =
  unless (BS.null bytes)
    (atomically $ modifyTVar' (readBuffer conn) (bytes <>))

waitForReadWorkerStopped :: Conn -> IO ()
waitForReadWorkerStopped conn =
  atomically $ do
    running <- readTVar (readWorkerRunning conn)
    when running retry