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