1module ProjectM36.TransactionGraph.Persist where
3import ProjectM36.TransactionGraph
4import ProjectM36.Transaction
5import ProjectM36.Transaction.Persist
7import ProjectM36.ScriptSession
8import ProjectM36.Persist (writeFileSync, renameSync, DiskSync)
9import ProjectM36.FileLock
10import System.Directory
13import Data.Time.Clock.POSIX
14import qualified Data.UUID as U
15import qualified Data.Set as S
16import qualified Data.Map as M
17import qualified Data.Text as T
18import Data.Text.Encoding
19import Control.Monad (foldM)
20import Data.Either (isRight)
21import Data.Maybe (fromMaybe)
22import qualified Data.List as L
23import Control.Exception.Base
24import qualified Data.ByteString as BS
25import qualified Data.Text.Encoding as TE
26import Data.ByteString (ByteString)
28import qualified Crypto.Hash.SHA256 as SHA256
32import System.FilePath.Glob
34type LockFileHash = ByteString
37The "m36vX" file at the top-level of the destination directory contains the the transaction graph as a set of transaction ids referencing their parents (1 or more)
38Each Transaction is written to it own directory named by its transaction id. Partially written transactions ids are prefixed with a "." to indicate incompleteness in the graph.
40Persistence requires a POSIX-compliant, journaled-metadata filesystem.
46transactionLogFileName :: FilePath
47transactionLogFileName = "m36v" ++ show expectedVersion
49transactionLogPath :: FilePath -> FilePath
50transactionLogPath dbdir = dbdir </> transactionLogFileName
52headsPath :: FilePath -> FilePath
53headsPath dbdir = dbdir </> "heads"
55lockFilePath :: FilePath -> FilePath
56lockFilePath dbdir = dbdir </> "lockFile"
59verify that the database directory is valid or bootstrap it
60-note: checking for the existence of every transaction may be prohibitively expensive
61- return error or lock file handle which is already locked with a read lock
64checkForOtherVersions :: FilePath -> IO (Either PersistenceError ())
65checkForOtherVersions dbdir = do
66 versionMatches <- globDir1 (compile "m36v*") dbdir
67 let otherVersions = L.delete transactionLogFileName (map takeFileName versionMatches)
68 if not (null otherVersions) then
69 pure (Left (WrongDatabaseFormatVersionError transactionLogFileName (head otherVersions)))
74setupDatabaseDir :: DiskSync -> FilePath -> TransactionGraph -> IO (Either PersistenceError (LockFile, LockFileHash))
75setupDatabaseDir sync dbdir bootstrapGraph = do
76 dbdirExists <- doesDirectoryExist dbdir
77 eWrongVersion <- checkForOtherVersions dbdir
79 Left err -> pure (Left err)
81 m36exists <- doesFileExist (transactionLogPath dbdir)
82 if dbdirExists && m36exists then do
83 --no directories to write, just
84 locker <- openLockFile (lockFilePath dbdir)
85 gDigest <- bracket_ (lockFile locker WriteLock) (unlockFile locker) (readGraphTransactionIdFileDigest dbdir)
86 pure (Right (locker, gDigest))
87 else if not m36exists then
88 Right <$> bootstrapDatabaseDir sync dbdir bootstrapGraph
90 pure (Left (InvalidDirectoryError dbdir))
92initialize a database directory with the graph from which to bootstrap- return lock file handle which must be closed by the caller
94bootstrapDatabaseDir :: DiskSync -> FilePath -> TransactionGraph -> IO (LockFile, LockFileHash)
95bootstrapDatabaseDir sync dbdir bootstrapGraph = do
97 locker <- openLockFile (lockFilePath dbdir)
98 let allTransIds = map transactionId (S.toList (transactionsForGraph bootstrapGraph))
99 digest <- bracket_ (lockFile locker WriteLock) (unlockFile locker) (transactionGraphPersist sync dbdir allTransIds bootstrapGraph)
100 pure (locker, digest)
103incrementally updates an existing database directory
105--assume that all transaction data has already been written
106-assume that all non-head transactions have already been written because this is an incremental (and concurrent!) write method
107--store the head names with a symlink to the transaction under "heads"
109transactionGraphPersist :: DiskSync -> FilePath -> [TransactionId] -> TransactionGraph -> IO LockFileHash
110transactionGraphPersist sync destDirectory transIds graph = do
111 transactionsPersist sync transIds destDirectory graph
113 newDigest <- writeGraphTransactionIdFile sync destDirectory graph
115 transactionGraphHeadsPersist sync destDirectory graph
118-- | The incremental writer writes the transactions ids specified by the second argument.
120-- There was a bug here via #128 because automerge added multiple transactions to the graph but this function used to only write the head transactions from the graph. Automerge creates multiple transactions, so these are now passed in as the second argument.
121transactionsPersist :: DiskSync -> [TransactionId] -> FilePath -> TransactionGraph -> IO ()
122transactionsPersist sync transIds destDirectory graphIn = mapM_ writeTrans transIds
123 where writeTrans tid =
124 case transactionForId tid graphIn of
125 Left err -> error ("writeTransaction: " ++ show err)
126 Right trans -> writeTransaction sync destDirectory trans
129write graph heads to a file which can be atomically swapped
131--writing the heads in a directory is a synchronization nightmare, so just write the binary to a file and swap atomically
132transactionGraphHeadsPersist :: DiskSync -> FilePath -> TransactionGraph -> IO ()
133transactionGraphHeadsPersist sync dbdir graph = do
134 let headFileStr :: (HeadName, Transaction) -> T.Text
135 headFileStr (headName, trans) = headName <> " " <> U.toText (transactionId trans)
136 withTempDirectory dbdir ".heads.tmp" $ \tempHeadsDir -> do
137 let tempHeadsPath = tempHeadsDir </> "heads"
138 headsStrLines = map headFileStr $ M.toList (transactionHeadsForGraph graph)
139 writeFileSync sync tempHeadsPath $ T.intercalate "\n" headsStrLines
140 renameSync sync tempHeadsPath (headsPath dbdir)
142transactionGraphHeadsLoad :: FilePath -> IO [(HeadName,TransactionId)]
143transactionGraphHeadsLoad dbdir = do
144 headsData <- readFile (headsPath dbdir)
145 let headsAssocs = map (\l -> let [headName, uuidStr] = words l in
148 return [(T.pack headName, uuid) | (headName, Just uuid) <- map (second U.fromString) headsAssocs]
151load any transactions which are not already part of the incoming transaction graph
154transactionGraphLoad :: FilePath -> TransactionGraph -> Maybe ScriptSession -> IO (Either PersistenceError TransactionGraph)
155transactionGraphLoad dbdir graphIn mScriptSession = do
156 --optimization: perform tail-bisection search to find last-recorded transaction in the existing stream- replay the rest
157 --read in all missing transactions from transaction directories and add to graph
158 uuidInfo <- readGraphTransactionIdFile dbdir
159 freshHeadsAssoc <- transactionGraphHeadsLoad dbdir
161 Left err -> return $ Left err
163 let folder eitherGraph transId = case eitherGraph of
164 Left err -> return $ Left err
165 Right graph -> readTransactionIfNecessary dbdir transId mScriptSession graph
166 loadedGraph <- foldM folder (Right graphIn) (map (\(tid,_,_) -> tid) info)
168 Left err -> return $ Left err
169 Right freshGraph -> do
170 let maybeTransHeads = [(headName, transactionForId uuid freshGraph) | (headName, uuid) <- freshHeadsAssoc]
171 freshHeads = M.fromList [(headName,trans) | (headName, Right trans) <- maybeTransHeads]
172 return $ Right $ TransactionGraph freshHeads (transactionsForGraph freshGraph)
175if the transaction with the TransactionId argument is not yet part of the graph, then read the transaction and add it - this does not update the heads
177readTransactionIfNecessary :: FilePath -> TransactionId -> Maybe ScriptSession -> TransactionGraph -> IO (Either PersistenceError TransactionGraph)
178readTransactionIfNecessary dbdir transId mScriptSession graphIn =
179 if isRight $ transactionForId transId graphIn then
180 --the transaction is already known and loaded- done
181 return $ Right graphIn
183 trans <- readTransaction dbdir transId mScriptSession
185 Left err -> return $ Left err
186 Right trans' -> return $ Right $ TransactionGraph (transactionHeadsForGraph graphIn) (S.insert trans' (transactionsForGraph graphIn))
188writeGraphTransactionIdFile :: DiskSync -> FilePath -> TransactionGraph -> IO LockFileHash
189writeGraphTransactionIdFile sync destDirectory (TransactionGraph _ transSet) = writeFileSync sync graphFile uuidInfo >> pure digest
191 graphFile = transactionLogPath destDirectory
192 uuidInfo = T.intercalate "\n" graphLines
193 digest = SHA256.hash (encodeUtf8 uuidInfo)
194 graphLines = S.toList $ S.map graphLine transSet
195 epochTime = realToFrac . utcTimeToPOSIXSeconds . transactionTimestamp :: Transaction -> Double
196 graphLine trans = U.toText (transactionId trans)
198 <> T.pack (show (epochTime trans))
200 <> T.intercalate " " (S.toList (S.map U.toText $ transactionParentIds trans))
202readGraphTransactionIdFileDigest :: FilePath -> IO LockFileHash
203readGraphTransactionIdFileDigest dbdir = do
204 let graphTransactionIdData = readUTF8FileOrError (transactionLogPath dbdir)
205 SHA256.hash . encodeUtf8 <$> graphTransactionIdData
207readGraphTransactionIdFile :: FilePath -> IO (Either PersistenceError [(TransactionId, UTCTime, [TransactionId])])
208readGraphTransactionIdFile dbdir = do
209 --read in all transactions' uuids
210 let grapher line = let tid:epochText:parentIds = T.words line in
211 (readUUID tid, readEpoch epochText, map readUUID parentIds)
212 readUUID uuidText = fromMaybe (error "failed to read uuid") (U.fromText uuidText)
213 readEpoch t = posixSecondsToUTCTime (realToFrac (either (error "failed to read epoch") fst (double t)))
214 Right . map grapher . T.lines <$> readUTF8FileOrError (transactionLogPath dbdir)
216--rationale- reading essential database files must fail hard
217readUTF8FileOrError :: FilePath -> IO T.Text
218readUTF8FileOrError pathIn = do
219 eFileBytes <- try (BS.readFile pathIn) :: IO (Either IOError BS.ByteString)
221 Left err -> error (show err)
223 case TE.decodeUtf8' fileBytes of
224 Left err -> error (show err)
225 Right utf8Bytes -> pure utf8Bytes
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list