acid-state-0.3: src/Data/Acid/Log.hs
-- A log is a stack of entries that supports efficient pushing of
-- new entries and fetching of old. It can be considered an
-- extendible array of entries.
--
module Data.Acid.Log
( FileLog
, LogKey(..)
, EntryId
, openFileLog
, closeFileLog
, pushEntry
, readEntriesFrom
, newestEntry
, askCurrentEntryId
) where
import Data.Acid.Archive as Archive
import System.Directory
import System.FilePath
import System.IO
import Control.Monad
import Control.Concurrent
import Control.Concurrent.STM
import qualified Data.ByteString.Lazy as Lazy
--import qualified Data.ByteString as Strict
import Data.List
import Data.Maybe
import Data.Binary
import Text.Printf ( printf )
import Paths_acid_state ( version )
import Data.Version ( showVersion )
type EntryId = Int
data FileLog object
= FileLog { logIdentifier :: LogKey object
, logCurrent :: MVar (Handle)
, logNextEntryId :: TVar EntryId
, logQueue :: TVar ([Lazy.ByteString], [IO ()])
, logThreads :: [ThreadId]
}
data LogKey object
= LogKey
{ logDirectory :: FilePath
, logPrefix :: String
}
formatLogFile :: String -> EntryId -> String
formatLogFile tag n
= printf "%s-%010d.log" tag n
findLogFiles :: LogKey object -> IO [(EntryId, FilePath)]
findLogFiles identifier
= do createDirectoryIfMissing True (logDirectory identifier)
files <- getDirectoryContents (logDirectory identifier)
return [ (tid, logDirectory identifier </> file)
| file <- files
, logFile <- maybeToList (stripPrefix (logPrefix identifier ++ "-") file)
, (tid, ".log") <- reads logFile ]
saveVersionFile :: LogKey object -> IO ()
saveVersionFile key
= do exist <- doesFileExist versionFile
unless exist $ writeFile versionFile (showVersion version)
where versionFile = logDirectory key </> logPrefix key <.> "version"
openFileLog :: LogKey object -> IO (FileLog object)
openFileLog identifier
= do logFiles <- findLogFiles identifier
saveVersionFile identifier
currentState <- newEmptyMVar
queue <- newTVarIO ([], [])
nextEntryRef <- newTVarIO 0
tid2 <- forkIO $ fileWriter currentState queue
let log = FileLog { logIdentifier = identifier
, logCurrent = currentState
, logNextEntryId = nextEntryRef
, logQueue = queue
, logThreads = [tid2] }
if null logFiles
then do let currentEntryId = 0
currentHandle <- openBinaryFile (logDirectory identifier </> formatLogFile (logPrefix identifier) currentEntryId) WriteMode
putMVar currentState currentHandle
else do let (lastFileEntryId, lastFilePath) = maximum logFiles
entries <- readEntities lastFilePath
let currentEntryId = lastFileEntryId + length entries
atomically $ writeTVar nextEntryRef currentEntryId
currentHandle <- openFile (logDirectory identifier </> formatLogFile (logPrefix identifier) currentEntryId) WriteMode
putMVar currentState currentHandle
return log
fileWriter currentState queue
= forever $
do (entries, actions) <- atomically $ do (entries, actions) <- readTVar queue
when (null entries && null actions) retry
writeTVar queue ([], [])
-- We don't actually have to reverse the actions
-- but I don't think it hurts performance much.
return (reverse entries, reverse actions)
withMVar currentState $ \handle ->
do let arch = Archive.packEntries entries
Lazy.hPutStr handle arch
hFlush handle
return ()
sequence_ actions
yield
closeFileLog :: FileLog object -> IO ()
closeFileLog log
= modifyMVar_ (logCurrent log) $ \handle ->
do hClose handle
forkIO $ forM_ (logThreads log) killThread
return $ error "FileLog has been closed"
readEntities :: FilePath -> IO [Lazy.ByteString]
readEntities path
= do archive <- Lazy.readFile path
return $ worker (Archive.readEntries archive)
where worker Done = []
worker (Next entry next)
= entry : worker next
worker Fail{} = []
-- Read all durable entries younger than the given EntryId.
-- Note that entries written during or after this call won't
-- be included in the returned list.
readEntriesFrom :: Binary object => FileLog object -> EntryId -> IO [object]
readEntriesFrom log youngestEntry
= do -- Cut the log so we can read written entries without interfering
-- with the writing of new entries.
entryCap <- cutFileLog log
-- We're interested in these entries: youngestEntry <= x < entryCap.
logFiles <- findLogFiles (logIdentifier log)
let sorted = sort logFiles
findRelevant [] = []
findRelevant [ logFile ]
| youngestEntry <= rangeStart logFile && rangeStart logFile < entryCap
= [ logFile ]
| otherwise
= []
findRelevant ( left : right : xs )
| youngestEntry >= rangeStart right -- All entries in 'path' must be too old if this is true
= findRelevant (right : xs)
| rangeStart left >= entryCap -- All files from now on contain entries that are too young.
= []
| otherwise
= left : findRelevant (right : xs)
relevant = findRelevant sorted
firstEntryId = case relevant of
[] -> 0
( logFile : _logFiles) -> rangeStart logFile
archive <- liftM Lazy.concat $ mapM Lazy.readFile (map snd relevant)
let entries = entriesToList $ readEntries archive
return $ map decode
$ take (entryCap - youngestEntry) -- Take events under the eventCap.
$ drop (youngestEntry - firstEntryId) entries -- Drop entries that are too young.
where rangeStart (firstEntryId, _path) = firstEntryId
cutFileLog :: FileLog object -> IO EntryId
cutFileLog log
= do mvar <- newEmptyMVar
let action = do currentEntryId <- atomically $
do (entries, _) <- readTVar (logQueue log)
next <- readTVar (logNextEntryId log)
return (next - length entries)
modifyMVar_ (logCurrent log) $ \old ->
do hClose old
openFile (logDirectory key </> formatLogFile (logPrefix key) currentEntryId) WriteMode
putMVar mvar currentEntryId
atomically $
do (entries, actions) <- readTVar (logQueue log)
writeTVar (logQueue log) (entries, action : actions)
takeMVar mvar
where key = logIdentifier log
-- Finds the newest entry in the log. Doesn't work on open logs.
-- Do not use after the log has been opened.
-- Implementation: Search the newest log files first. Once a file
-- containing at least one valid entry is found,
-- return the last entry in that file.
newestEntry :: Binary object => LogKey object -> IO (Maybe object)
newestEntry identifier
= do logFiles <- findLogFiles identifier
let sorted = reverse $ sort logFiles
(eventIds, files) = unzip sorted
archives <- mapM Lazy.readFile files
return $ worker archives
where worker [] = Nothing
worker (archive:archives)
= case Archive.readEntries archive of
Done -> worker archives
Next entry next -> Just (decode (lastEntry entry next))
Fail{} -> worker archives
lastEntry entry Done = entry
lastEntry entry Fail{} = entry
lastEntry _ (Next entry next) = lastEntry entry next
-- Schedule a new log entry. This call does not block
-- The given IO action runs once the object is durable. The IO action
-- blocks the serialization of events so it should be swift.
pushEntry :: Binary object => FileLog object -> object -> IO () -> IO ()
pushEntry log object finally
= atomically $
do tid <- readTVar (logNextEntryId log)
writeTVar (logNextEntryId log) (tid+1)
(entries, actions) <- readTVar (logQueue log)
writeTVar (logQueue log) ( encoded : entries, finally : actions )
where encoded = encode object
askCurrentEntryId :: FileLog object -> IO EntryId
askCurrentEntryId log
= atomically $ readTVar (logNextEntryId log)