{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module Free.Agent.Bus.File
(
cursorPath,
readCursor,
writeCursor,
QuiesceConfig (..),
Flow (..),
tailLog,
)
where
import Circuit.Agent (Name, Post (..), PostId, deliversTo)
import Circuit.Agent.Framing (Stamped, stamp, stamped, unframeStored)
import Control.Concurrent (threadDelay)
import Control.Concurrent.STM
( TMVar,
atomically,
check,
newTMVarIO,
newTVarIO,
orElse,
readTVar,
readTVarIO,
takeTMVar,
tryPutTMVar,
writeTVar,
)
import Control.Monad (guard, unless, when)
import Data.ByteString qualified as BS
import Data.IORef (newIORef, readIORef, writeIORef)
import Data.Maybe (mapMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Encoding qualified as TE
import Data.Text.IO qualified as TIO
import System.Directory (doesFileExist)
import System.FSNotify (Event (..), watchDir, withManager)
import System.FilePath (takeDirectory, takeFileName, (</>))
import System.IO
( IOMode (AppendMode, ReadMode),
SeekMode (AbsoluteSeek),
hSeek,
withFile,
)
import System.Timeout (timeout)
import Text.Read (readMaybe)
data QuiesceConfig = QuiesceConfig
{
QuiesceConfig -> Int
qcCycles :: Int,
QuiesceConfig -> Text
qcPitboss :: Name,
QuiesceConfig -> Int
qcCycleMicros :: Int
}
deriving (Int -> QuiesceConfig -> ShowS
[QuiesceConfig] -> ShowS
QuiesceConfig -> FilePath
(Int -> QuiesceConfig -> ShowS)
-> (QuiesceConfig -> FilePath)
-> ([QuiesceConfig] -> ShowS)
-> Show QuiesceConfig
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> QuiesceConfig -> ShowS
showsPrec :: Int -> QuiesceConfig -> ShowS
$cshow :: QuiesceConfig -> FilePath
show :: QuiesceConfig -> FilePath
$cshowList :: [QuiesceConfig] -> ShowS
showList :: [QuiesceConfig] -> ShowS
Show)
data Flow = Continue | Halt
deriving (Flow -> Flow -> Bool
(Flow -> Flow -> Bool) -> (Flow -> Flow -> Bool) -> Eq Flow
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: Flow -> Flow -> Bool
== :: Flow -> Flow -> Bool
$c/= :: Flow -> Flow -> Bool
/= :: Flow -> Flow -> Bool
Eq, Int -> Flow -> ShowS
[Flow] -> ShowS
Flow -> FilePath
(Int -> Flow -> ShowS)
-> (Flow -> FilePath) -> ([Flow] -> ShowS) -> Show Flow
forall a.
(Int -> a -> ShowS) -> (a -> FilePath) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Flow -> ShowS
showsPrec :: Int -> Flow -> ShowS
$cshow :: Flow -> FilePath
show :: Flow -> FilePath
$cshowList :: [Flow] -> ShowS
showList :: [Flow] -> ShowS
Show)
cursorPath :: FilePath -> Name -> FilePath
cursorPath :: FilePath -> Text -> FilePath
cursorPath FilePath
root Text
name = FilePath
root FilePath -> ShowS
</> (FilePath
".cursor-" FilePath -> ShowS
forall a. Semigroup a => a -> a -> a
<> Text -> FilePath
T.unpack Text
name)
readCursor :: FilePath -> Name -> IO PostId
readCursor :: FilePath -> Text -> IO PostId
readCursor FilePath
root Text
name = do
let path :: FilePath
path = FilePath -> Text -> FilePath
cursorPath FilePath
root Text
name
exists <- FilePath -> IO Bool
doesFileExist FilePath
path
if not exists
then latestPostId root
else do
txt <- TIO.readFile path
pure $ maybe 0 fromIntegral (readMaybe @Integer (T.unpack (T.strip txt)))
latestPostId :: FilePath -> IO PostId
latestPostId :: FilePath -> IO PostId
latestPostId FilePath
root = do
let logPath :: FilePath
logPath = FilePath
root FilePath -> ShowS
</> FilePath
"log.jsonl"
logExists <- FilePath -> IO Bool
doesFileExist FilePath
logPath
if not logExists
then pure 0
else do
txt <- TIO.readFile logPath
let ls = (Text -> Bool) -> [Text] -> [Text]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not (Bool -> Bool) -> (Text -> Bool) -> Text -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Text -> Bool
T.null) (Text -> [Text]
T.lines Text
txt)
case ls of
[] -> PostId -> IO PostId
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure PostId
0
[Text]
_ -> case forall a. PostBody a => Text -> Maybe (Stamped a)
unframeStored @Text ([Text] -> Text
forall a. HasCallStack => [a] -> a
last [Text]
ls) of
Just Stamped Text
stored -> PostId -> IO PostId
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ((UTCTime, PostId) -> PostId
forall a b. (a, b) -> b
snd (Stamped Text -> (UTCTime, PostId)
forall r a. Stamped r a -> r
stamp Stamped Text
stored) PostId -> PostId -> PostId
forall a. Num a => a -> a -> a
+ PostId
1)
Maybe (Stamped Text)
Nothing -> PostId -> IO PostId
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure (Int -> PostId
forall a b. (Integral a, Num b) => a -> b
fromIntegral ([Text] -> Int
forall a. [a] -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length [Text]
ls))
writeCursor :: FilePath -> Name -> PostId -> IO ()
writeCursor :: FilePath -> Text -> PostId -> IO ()
writeCursor FilePath
root Text
name PostId
pid =
FilePath -> Text -> IO ()
TIO.writeFile (FilePath -> Text -> FilePath
cursorPath FilePath
root Text
name) (FilePath -> Text
T.pack (PostId -> FilePath
forall a. Show a => a -> FilePath
show PostId
pid))
tailLog ::
FilePath ->
[Name] ->
PostId ->
Maybe (QuiesceConfig, IO ()) ->
(Stamped Text -> IO Flow) ->
IO ()
tailLog :: FilePath
-> [Text]
-> PostId
-> Maybe (QuiesceConfig, IO ())
-> (Stamped Text -> IO Flow)
-> IO ()
tailLog FilePath
path [Text]
names PostId
startCursor Maybe (QuiesceConfig, IO ())
mQuiesce Stamped Text -> IO Flow
cb = do
exists <- FilePath -> IO Bool
doesFileExist FilePath
path
unless exists $ do
withFile path AppendMode (\Handle
_ -> () -> IO ()
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ())
(off0, halted0) <- drainFrom 0 (filterStoredSince startCursor)
offRef <- newIORef off0
halted <- newTVarIO halted0
let logName = ShowS
takeFileName FilePath
path
dir = ShowS
takeDirectory FilePath
path
withManager $ \WatchManager
mgr -> do
signal <- () -> IO (TMVar ())
forall a. a -> IO (TMVar a)
newTMVarIO ()
busy <- newTVarIO True
_ <- watchDir mgr dir (\Event
ev -> ShowS
takeFileName (Event -> FilePath
eventPath Event
ev) FilePath -> FilePath -> Bool
forall a. Eq a => a -> a -> Bool
== FilePath
logName) $ \Event
_ev -> do
already <- TVar Bool -> IO Bool
forall a. TVar a -> IO a
readTVarIO TVar Bool
halted
unless already $ do
atomically $ writeTVar busy True
off <- readIORef offRef
(off', h) <- drainFrom off filterStored
writeIORef offRef off'
atomically $ do
when h (writeTVar halted True)
writeTVar busy False
_ <- tryPutTMVar signal ()
pure ()
case mQuiesce of
Maybe (QuiesceConfig, IO ())
Nothing -> IORef Integer -> TMVar () -> TVar Bool -> IO ()
pollLoop IORef Integer
offRef TMVar ()
signal TVar Bool
halted
Just (QuiesceConfig
qc, IO ()
onQuiesce) -> QuiesceConfig
-> TMVar () -> TVar Bool -> TVar Bool -> Int -> IO () -> IO ()
forall {a}.
QuiesceConfig
-> TMVar a -> TVar Bool -> TVar Bool -> Int -> IO () -> IO ()
quiesceLoop QuiesceConfig
qc TMVar ()
signal TVar Bool
busy TVar Bool
halted Int
0 IO ()
onQuiesce
where
pollLoop :: IORef Integer -> TMVar () -> TVar Bool -> IO ()
pollLoop IORef Integer
offRef TMVar ()
signal TVar Bool
halted = do
h <- TVar Bool -> IO Bool
forall a. TVar a -> IO a
readTVarIO TVar Bool
halted
if h
then pure ()
else do
m <- timeout 1_000_000 (atomically (takeTMVar signal))
case m of
Just () -> do
off <- IORef Integer -> IO Integer
forall a. IORef a -> IO a
readIORef IORef Integer
offRef
(off', h') <- drainFrom off filterStored
writeIORef offRef off'
when h' (atomically (writeTVar halted True))
Maybe ()
Nothing -> do
off <- IORef Integer -> IO Integer
forall a. IORef a -> IO a
readIORef IORef Integer
offRef
(off', h') <- drainFrom off filterStored
writeIORef offRef off'
when h' (atomically (writeTVar halted True))
pollLoop offRef signal halted
drainFrom :: Integer -> (Text -> Maybe (Stamped Text)) -> IO (Integer, Bool)
drainFrom Integer
off Text -> Maybe (Stamped Text)
filt = do
(ls, off') <- Integer -> IO ([Text], Integer)
readCompleteLines Integer
off
halted <- deliver (mapMaybe filt ls)
pure (off', halted)
deliver :: [Stamped Text] -> IO Bool
deliver [] = Bool -> IO Bool
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Bool
False
deliver (Stamped Text
p : [Stamped Text]
ps) = do
flow <- Stamped Text -> IO Flow
cb Stamped Text
p
case flow of
Flow
Halt -> Bool -> IO Bool
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Bool
True
Flow
Continue -> [Stamped Text] -> IO Bool
deliver [Stamped Text]
ps
readCompleteLines :: Integer -> IO ([Text], Integer)
readCompleteLines Integer
off = FilePath
-> IOMode
-> (Handle -> IO ([Text], Integer))
-> IO ([Text], Integer)
forall r. FilePath -> IOMode -> (Handle -> IO r) -> IO r
withFile FilePath
path IOMode
ReadMode ((Handle -> IO ([Text], Integer)) -> IO ([Text], Integer))
-> (Handle -> IO ([Text], Integer)) -> IO ([Text], Integer)
forall a b. (a -> b) -> a -> b
$ \Handle
h -> do
Handle -> SeekMode -> Integer -> IO ()
hSeek Handle
h SeekMode
AbsoluteSeek Integer
off
bs <- Handle -> IO ByteString
BS.hGetContents Handle
h
pure (completeLines off bs)
completeLines :: b -> ByteString -> ([Text], b)
completeLines b
off ByteString
bs =
case Word8 -> ByteString -> Maybe Int
BS.elemIndexEnd Word8
0x0A ByteString
bs of
Maybe Int
Nothing -> ([], b
off)
Just Int
i ->
( Text -> [Text]
T.lines (ByteString -> Text
TE.decodeUtf8 (Int -> ByteString -> ByteString
BS.take (Int
i Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1) ByteString
bs)),
b
off b -> b -> b
forall a. Num a => a -> a -> a
+ Int -> b
forall a b. (Integral a, Num b) => a -> b
fromIntegral Int
i b -> b -> b
forall a. Num a => a -> a -> a
+ b
1
)
filterStoredSince :: PostId -> Text -> Maybe (Stamped Text)
filterStoredSince PostId
cursor Text
line = do
stored <- forall a. PostBody a => Text -> Maybe (Stamped a)
unframeStored @Text Text
line
guard (snd (stamp stored) >= cursor)
guard (deliversTo (stamped stored) names)
pure stored
filterStored :: Text -> Maybe (Stamped Text)
filterStored Text
line = do
stored <- forall a. PostBody a => Text -> Maybe (Stamped a)
unframeStored @Text Text
line
if deliversTo (stamped stored) names then Just stored else Nothing
awaitEvent :: TMVar a -> TVar Bool -> STM Bool
awaitEvent TMVar a
signal TVar Bool
halted =
(TVar Bool -> STM Bool
forall a. TVar a -> STM a
readTVar TVar Bool
halted STM Bool -> (Bool -> STM ()) -> STM ()
forall a b. STM a -> (a -> STM b) -> STM b
forall (m :: * -> *) a b. Monad m => m a -> (a -> m b) -> m b
>>= Bool -> STM ()
check STM () -> STM Bool -> STM Bool
forall a b. STM a -> STM b -> STM b
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> Bool -> STM Bool
forall a. a -> STM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Bool
True)
STM Bool -> STM Bool -> STM Bool
forall a. STM a -> STM a -> STM a
`orElse` (TMVar a -> STM a
forall a. TMVar a -> STM a
takeTMVar TMVar a
signal STM a -> STM Bool -> STM Bool
forall a b. STM a -> STM b -> STM b
forall (m :: * -> *) a b. Monad m => m a -> m b -> m b
>> Bool -> STM Bool
forall a. a -> STM a
forall (f :: * -> *) a. Applicative f => a -> f a
pure Bool
False)
quiesceLoop :: QuiesceConfig
-> TMVar a -> TVar Bool -> TVar Bool -> Int -> IO () -> IO ()
quiesceLoop QuiesceConfig
qc TMVar a
signal TVar Bool
busy TVar Bool
halted Int
count IO ()
onQuiesce = do
m <- Int -> IO Bool -> IO (Maybe Bool)
forall a. Int -> IO a -> IO (Maybe a)
timeout (QuiesceConfig -> Int
qcCycleMicros QuiesceConfig
qc) (STM Bool -> IO Bool
forall a. STM a -> IO a
atomically (TMVar a -> TVar Bool -> STM Bool
forall {a}. TMVar a -> TVar Bool -> STM Bool
awaitEvent TMVar a
signal TVar Bool
halted))
case m of
Just Bool
True -> () -> IO ()
forall a. a -> IO a
forall (f :: * -> *) a. Applicative f => a -> f a
pure ()
Just Bool
False -> QuiesceConfig
-> TMVar a -> TVar Bool -> TVar Bool -> Int -> IO () -> IO ()
quiesceLoop QuiesceConfig
qc TMVar a
signal TVar Bool
busy TVar Bool
halted Int
0 IO ()
onQuiesce
Maybe Bool
Nothing -> do
inProgress <- TVar Bool -> IO Bool
forall a. TVar a -> IO a
readTVarIO TVar Bool
busy
if inProgress
then quiesceLoop qc signal busy halted 0 onQuiesce
else do
let count' = Int
count Int -> Int -> Int
forall a. Num a => a -> a -> a
+ Int
1
if count' >= qcCycles qc
then onQuiesce
else quiesceLoop qc signal busy halted count' onQuiesce