lattest-lib-0.1.0.0: src/Lattest/Streams/Synchronized.hs
module Lattest.Streams.Synchronized (
TInputStream,
read,
readAll,
unRead,
hasInput,
fromInputStreamBuffered,
makeTInputStream,
fromBuffer,
fromTMVar,
duplicate,
map,
mapUnbuffered,
mergeBuffered,
mergeBufferedWith,
consumeBufferedWith,
Streamed(..),
tryRead,
tryReadIO,
tryReadIO'
)
where
import Prelude hiding (read, map)
import Control.Concurrent.STM(STM, retry)
import Control.Concurrent.STM.TQueue(TQueue, newTQueueIO, writeTQueue, readTQueue, isEmptyTQueue, unGetTQueue)
import Control.Concurrent.STM.TMVar(TMVar, takeTMVar, isEmptyTMVar)
import Control.Concurrent.STM.TVar(newTVarIO, readTVar, writeTVar)
import GHC.IO.Exception (ioe_location, ioe_errno)
import Control.Exception(throwIO, Exception, catchJust)
import Control.Monad (void, when)
import Data.List(singleton)
import GHC.Conc (atomically, forkIO)
import System.IO.Streams (InputStream, OutputStream, makeOutputStream, connect)
import qualified System.IO.Streams as Streams (write)
import Control.Monad.Extra((||^))
import Foreign.C.Error (eBADF, Errno (Errno))
data TInputStream a = TInputStream {
tRead :: STM (Maybe a),
tUnRead :: a -> STM (),
tHasInput :: STM Bool -- whether this stream has a (Maybe a) available right now, *or* whether it is closed. Note: it may be tempting
-- to omit this field, and just check whether tRead retries, but that is ambiguous: tRead will retry either if
-- there is no input yet, *or* if someone is writing to the unknown TVars used to implement tRead. The first case
-- would not have an input, the second case would.
}
read :: TInputStream a -> STM (Maybe a)
read = tRead
hasInput :: TInputStream a -> STM Bool
hasInput = tHasInput
-- read inputs from the stream until it has no more input
readAll :: TInputStream a -> STM [Maybe a]
readAll tis = reverse <$> readAll' []
where
readAll' acc = do
readAttempt <- tryRead tis
case readAttempt of
StreamClosed -> return (Nothing:acc)
Unavailable -> return acc
Available next -> readAll' (Just next:acc)
unRead :: a -> TInputStream a -> STM ()
unRead = flip tUnRead
data Streamed a = Available a | Unavailable | StreamClosed deriving (Eq, Ord, Show, Read)
tryRead :: TInputStream a -> STM (Streamed a)
tryRead tis = do
ready <- hasInput tis
if ready
then do
minput <- read tis
return $ maybe StreamClosed Available minput
else return Unavailable
tryReadIO :: TInputStream a -> IO (Streamed a)
tryReadIO = atomically . tryRead
data TInputStreamClosedException = TInputStreamClosedException deriving (Eq, Ord, Show)
instance Exception TInputStreamClosedException
tryReadIO' :: TInputStream a -> IO (Maybe a)
tryReadIO' tis = do
streamed <- tryReadIO tis
case streamed of
Available a -> return $ Just a
Unavailable -> return Nothing
StreamClosed -> throwIO TInputStreamClosedException
makeTInputStream :: STM (Maybe a) -> STM Bool -> IO (TInputStream a)
makeTInputStream readInput hasInput' = do
pushbackStack <- newTQueueIO
withClosedState $ TInputStream {
tRead = do
hasPushback <- isNonEmptyTQueue pushbackStack
if hasPushback
then Just <$> readTQueue pushbackStack
else readInput,
tUnRead = unGetTQueue pushbackStack,
tHasInput = isNonEmptyTQueue pushbackStack ||^ hasInput'
}
withClosedState :: TInputStream a -> IO (TInputStream a)
withClosedState stream = do
closeVar <- newTVarIO False
return $ TInputStream {
tRead = do
closed <- readTVar closeVar
if closed
then return Nothing
else do
mi <- tRead stream
case mi of
Just i -> return $ Just i
Nothing -> do
writeTVar closeVar True
return Nothing,
tUnRead = tUnRead stream,
tHasInput = do
closed <- readTVar closeVar
if closed
then return True
else tHasInput stream
}
fromTMVar :: TMVar (Maybe a) -> IO (TInputStream a)
fromTMVar tmvar = makeTInputStream (takeTMVar tmvar) (not <$> isEmptyTMVar tmvar)
-- note: the buffer is also used to push back values. Thus, do not read this buffer from outside the resulting stream, as then this stream may
-- violate the principle that pushed back values are read again.
fromBuffer :: TQueue (Maybe a) -> IO (TInputStream a)
fromBuffer buffer = withClosedState $ TInputStream {
tRead = readTQueue buffer,
tUnRead = unGetTQueue buffer . Just,
tHasInput = isNonEmptyTQueue buffer
}
-- a synchronized input stream that reads from the given input stream. A monitoring thread reads the given input stream and buffers the inputs on the background
-- TODO support a bound for the buffer
fromInputStreamBuffered :: InputStream a -> IO (TInputStream a)
fromInputStreamBuffered is = do
buffer <- newTQueueIO
bufferOS <- makeOutputStream $ atomically . writeTQueue buffer -- an intermediate output stream to write to the queue
void $ forkIO $ catchSocketClosed $ connect is bufferOS -- move items from the original adapter into the buffer in a separate thread
fromBuffer buffer
where
-- catches "threadWait: invalid argument (Bad file descriptor)"
-- which is thrown when the socket is closed while this thread is running, e.g. on Adaptor.close()
-- The alternative solution is to keep track of the threadID given by forkIO,
-- and explicitly stop the thread before closing the socket.
catchSocketClosed forkedThread = catchJust threadwaitinvalid forkedThread pure
threadwaitinvalid e
| ioe_location e == "threadWait"
, fmap Errno (ioe_errno e) == Just eBADF = Just ()
| otherwise = Nothing
mapUnbuffered :: (a -> b) -> (b -> a) -> TInputStream a -> TInputStream b -- we need an inverse to push a's back to the original TInputStream of b's
mapUnbuffered f f' tis = TInputStream { -- FIXME either remove or wrap in withClosedState
tRead = fmap f <$> read tis,
tUnRead = tUnRead tis . f',
tHasInput = tHasInput tis
}
map :: (a -> b) -> TInputStream a -> IO (TInputStream b)
map f tis = makeTInputStream (fmap f <$> read tis) (hasInput tis)
-- Take inputs from both given input streams. If both streams have an input available, the first stream takes precedence. The resulting stream closes
-- if either given stream closes. The merge is buffered, so that the sequence of arrival of inputs in both streams is preserved, even if the resulting
-- stream is not read.
mergeBuffered :: TInputStream a -> TInputStream a -> IO (TInputStream a)
mergeBuffered = mergeBufferedWith $ \tis1 tis2 -> singleton <$> do
ready1 <- hasInput tis1
if ready1
then read tis1
else do
ready2 <- hasInput tis2
if ready2
then read tis2
else retry
-- Take inputs from both given input streams, using the given combinator function. The merge is buffered, so that the sequence of arrival of inputs in both streams is preserved,
-- even if the resulting stream is not read. The resulting stream closes if either given stream closes.
mergeBufferedWith :: (TInputStream a -> TInputStream a -> STM [Maybe a]) -> TInputStream a -> TInputStream a -> IO (TInputStream a)
mergeBufferedWith merge tis1 tis2 = consumeBufferedWith (merge tis1 tis2) (hasAnyInput tis1 tis2) -- FIXME if one stream closes, then no further inputs should be read from the other stream, and only Nothings should be returned instead
hasAnyInput :: TInputStream a -> TInputStream a -> STM Bool
hasAnyInput tis1 tis2 = hasInput tis1 ||^ hasInput tis2
-- Actively take inputs from the given list STM, via a separate thread. This ensures that ordering from the given list STM is preserved as much as possible,
-- in case of concurrent inputs. The resulting stream only reads via the separate thread if needed, and reads from the list STM directly if this is faster.
-- The resulting stream closes if the consumer closes. This function assumes that the given STMs close properly, i.e., no Just's are sent after a Nothing
consumeBufferedWith :: STM [Maybe a] -> STM Bool -> IO (TInputStream a)
consumeBufferedWith producer producerHasInput = do
buffer <- newTQueueIO
let writeToBuffer = writeTQueue buffer
--bufferOS <- makeOutputStream $ \x -> putStrLn "Synchronized.merging" >> (atomically . writeTQueue buffer) x -- an intermediate output stream to write to the queue
void $ forkIO $ bufferLoop writeToBuffer -- move items from the original streams into the buffer in a separate thread
bufferIS <- fromBuffer buffer
return $ bufferIS {
tRead = pickOneAndMergeRest buffer writeToBuffer,
tHasInput = hasInput bufferIS ||^ producerHasInput
}
where
bufferLoop writeToBuffer = do
continue <- atomically $ do
produced <- producer
writeSeqTo produced writeToBuffer
when continue $ bufferLoop writeToBuffer
writeSeqTo [] _ = return True -- TODO this is an OutputStream utils function, similar to Streams.write/writeTo, move to an appropriate module
writeSeqTo (Nothing:_) writeToBuffer = do
_ <- writeToBuffer Nothing
return False
writeSeqTo (Just a:as) writeToBuffer = do
_ <- writeToBuffer (Just a)
writeSeqTo as writeToBuffer
pickOneAndMergeRest buffer writeToBuffer = do
bufferHasInput <- not <$> isEmptyTQueue buffer
produced <- if bufferHasInput
then singleton <$> readTQueue buffer
else producer
case produced of
[] -> retry
(Just x:xs) -> do
void $ writeSeqTo xs writeToBuffer
return $ Just x
(Nothing:_) -> do
unGetTQueue buffer Nothing
return Nothing
{-
-- TODO either remove or expose as nice util function, even though we don't seem to need this now
tconnect :: TInputStream a -> OutputStream a -> IO ()
tconnect tis os = tconnect'
where
tconnect' = do
mA <- atomically $ read tis
case mA of
Nothing -> Streams.write Nothing os
Just _ -> do
Streams.write mA os
tconnect'
-}
-- FIXME split to a separate module, conceptually there's nothing synchronized about this w.r.t. other OutputStreams
duplicate :: OutputStream a -> OutputStream a -> IO (OutputStream a)
duplicate out1 out2 = makeOutputStream $ \maybeVal -> do
Streams.write maybeVal out1
Streams.write maybeVal out2
isNonEmptyTQueue :: TQueue a -> STM Bool
isNonEmptyTQueue queue = not <$> isEmptyTQueue queue