on master help sign in
WIP compilation checkpoint

when the graph is changed behind the process' back, TransactionIsNotHeadError is returned unconditionally, which is a mistake- it should only return that error only if the same head is being contended

commit57faeae64a365806cfed9915ca936dcfd1d10529 authorAgentM <agentm@themactionfaction.com> dateSat 10 Jun 2017, 11:14 (9 years ago) parentb2c6aa4
9 files changed+230−88
project-m36.cabal+4−1 src/lib/ProjectM36/Client.hs+121−47 src/lib/ProjectM36/DataTypes/DateTime.hs−1 src/lib/ProjectM36/Error.hs+2−1 src/lib/ProjectM36/FileLock.hs+36 src/lib/ProjectM36/Persist.hs+5−4 src/lib/ProjectM36/Transaction/Persist.hs+1−3 src/lib/ProjectM36/TransactionGraph.hs+4−4 src/lib/ProjectM36/TransactionGraph/Persist.hs+57−27
project-m36.cabal+4−1
@@ -66,6 +66,8 @@ Library
6666 ,list-t
6767 ,optparse-applicative
6868 ,Glob
69+--used for hashing the transaction graph file to check for differences
70+ ,cryptonite >= 0.23
6971 Exposed-Modules: ProjectM36.Error,
7072 ProjectM36.Transaction,
7173 ProjectM36.TransactionGraph,
@@ -125,7 +127,8 @@ Library
125127 ProjectM36.Sessions,
126128 ProjectM36.Transaction.Persist,
127129 ProjectM36.TypeConstructor,
128− ProjectM36.TypeConstructorDef
130+ ProjectM36.TypeConstructorDef,
131+ ProjectM36.FileLock
129132 GHC-Options: -Wall
130133 if os(windows)
131134 Build-Depends: Win32 >= 2.3 && < 2.4
src/lib/ProjectM36/Client.hs+121−47
@@ -20,7 +20,7 @@ module ProjectM36.Client
2020 typeForRelationalExpr,
2121 inclusionDependencies,
2222 planForDatabaseContextExpr,
23− processPersistence,
23+ processTransactionGraphPersistence,
2424 currentSchemaName,
2525 SchemaName,
2626 HeadName,
@@ -101,6 +101,7 @@ import ProjectM36.TransactionGraph.Persist
101101import ProjectM36.Attribute hiding (atomTypes)
102102import ProjectM36.TransGraphRelationalExpression (TransGraphRelationalExpr, evalTransGraphRelationalExpr)
103103import ProjectM36.Persist (DiskSync(..))
104+import ProjectM36.FileLock
104105import ProjectM36.Notifications
105106import ProjectM36.Server.RemoteCallTypes
106107import qualified ProjectM36.DisconnectedTransaction as Discon
@@ -108,6 +109,8 @@ import ProjectM36.Relation (typesAsRelation)
108109import ProjectM36.ScriptSession (initScriptSession, ScriptSession)
109110import qualified ProjectM36.Relation as R
110111import qualified ProjectM36.DatabaseContext as DBC
112+import Control.Exception.Base
113+import GHC.Conc.Sync
111114
112115import Network.Transport (Transport(closeTransport))
113116import Network.Transport.TCP (createTransport, defaultTCPParameters, encodeEndPointAddress)
@@ -117,7 +120,6 @@ import Control.Distributed.Process.ManagedProcess.Client (call, safeCall)
117120import Control.Distributed.Process (NodeId(..), reconnect)
118121
119122import Data.UUID.V4 (nextRandom)
120−import Control.Concurrent.STM
121123import Data.Word
122124import Control.Distributed.Process (ProcessId, Process, receiveWait, send, match)
123125import Control.Exception (IOException, handle, AsyncException, throwIO, fromException, Exception)
@@ -134,6 +136,7 @@ import Data.Binary (Binary)
134136import GHC.Generics (Generic)
135137import Control.DeepSeq (force)
136138import System.IO
139+import Crypto.Hash
137140
138141type Hostname = String
139142
@@ -195,6 +198,9 @@ defaultRemoteConnectionInfo = RemoteProcessConnectionInfo defaultDatabaseName (c
195198-- | The 'Connection' represents either local or remote access to a database. All operations flow through the connection.
196199type ClientNodes = STMSet.Set ProcessId
197200
201+type TransactionGraphLockHandle = Handle
202+type LockFileHash = Digest SHA256
203+
198204-- internal structure specific to in-process connections
199205data InProcessConnectionConf = InProcessConnectionConf {
200206 ipPersistenceStrategy :: PersistenceStrategy,
@@ -203,7 +209,8 @@ data InProcessConnectionConf = InProcessConnectionConf {
203209 ipTransactionGraph :: TVar TransactionGraph,
204210 ipScriptSession :: Maybe ScriptSession,
205211 ipLocalNode :: LocalNode,
206− ipTransport :: Transport -- we hold onto this so that we can close it gracefully
212+ ipTransport :: Transport, -- we hold onto this so that we can close it gracefully
213+ ipLocks :: Maybe (TransactionGraphLockHandle, MVar LockFileHash) -- nothing when NoPersistence
207214 }
208215
209216data RemoteProcessConnectionConf = RemoteProcessConnectionConf {
@@ -275,14 +282,16 @@ connectProjectM36 (InProcessConnectionInfo strat notificationCallback ghcPkgPath
275282 (localNode, transport) <- createLocalNode
276283 notificationPid <- startNotificationListener localNode notificationCallback
277284 mScriptSession <- createScriptSession ghcPkgPaths
285+
278286 let conn = InProcessConnection (InProcessConnectionConf {
279− ipPersistenceStrategy =strat,
287+ ipPersistenceStrategy = strat,
280288 ipClientNodes = clientNodes,
281289 ipSessions = sessions,
282290 ipTransactionGraph = graphTvar,
283291 ipScriptSession = mScriptSession,
284292 ipLocalNode = localNode,
285− ipTransport = transport })
293+ ipTransport = transport,
294+ ipLocks = Nothing})
286295 addClientNode conn notificationPid
287296 pure (Right conn)
288297 MinimalPersistence dbdir -> connectPersistentProjectM36 strat NoDiskSync dbdir freshGraph notificationCallback ghcPkgPaths
@@ -318,8 +327,8 @@ connectPersistentProjectM36 :: PersistenceStrategy ->
318327connectPersistentProjectM36 strat sync dbdir freshGraph notificationCallback ghcPkgPaths = do
319328 err <- setupDatabaseDir sync dbdir freshGraph
320329 case err of
321− Just err' -> return $ Left (SetupDatabaseDirectoryError err')
322− Nothing -> do
330+ Left err' -> return $ Left (SetupDatabaseDirectoryError err')
331+ Right (lockFileH, digest) -> do
323332 mScriptSession <- createScriptSession ghcPkgPaths
324333 graph <- transactionGraphLoad dbdir emptyTransactionGraph mScriptSession
325334 case graph of
@@ -329,6 +338,7 @@ connectPersistentProjectM36 strat sync dbdir freshGraph notificationCallback ghc
329338 sessions <- STMMap.newIO
330339 clientNodes <- STMSet.newIO
331340 (localNode, transport) <- createLocalNode
341+ lockMVar <- newMVar digest
332342 let conn = InProcessConnection (InProcessConnectionConf {
333343 ipPersistenceStrategy = strat,
334344 ipClientNodes = clientNodes,
@@ -336,7 +346,8 @@ connectPersistentProjectM36 strat sync dbdir freshGraph notificationCallback ghc
336346 ipTransactionGraph = tvarGraph,
337347 ipScriptSession = mScriptSession,
338348 ipLocalNode = localNode,
339− ipTransport = transport
349+ ipTransport = transport,
350+ ipLocks = Just (lockFileH, lockMVar)
340351 })
341352
342353 notificationPid <- startNotificationListener localNode notificationCallback
@@ -578,6 +589,7 @@ executeGraphExprSTM_ freshId sessionId sessions graphExpr graphTvar = do
578589 Right session -> case evalGraphOp freshId (Sess.disconnectedTransaction session) graph graphExpr of
579590 Left err -> pure $ Left err
580591 Right (discon', graph') -> do
592+
581593 writeTVar graphTvar graph'
582594 let newSession = Session discon' (Sess.schemaName session)
583595 STMMap.insert newSession sessionId sessions
@@ -591,53 +603,98 @@ executeCommitExprSTM_ oldContext newContext nodes = do
591603 evaldNots = M.map mkEvaldNot fireNots
592604 mkEvaldNot notif = EvaluatedNotification { notification = notif, reportRelation = evalState (RE.evalRelationalExpr (reportExpr notif)) (RE.mkRelationalExprState oldContext) }
593605 pure (evaldNots, nodes)
594−
595−
606+
596607-- | Execute a transaction graph expression in the context of the session and connection. Transaction graph operators modify the transaction graph state.
608+-- OPTIMIZATION OPPORTUNITY: no locks are required to write new transaction data, only to update the transaction graph id file
609+-- if writing data is re-entrant, we may be able to use unsafeIOtoSTM
610+-- perhaps keep hash of data file instead of checking if our head was updated on every write
597611executeGraphExpr :: SessionId -> Connection -> TransactionGraphOperator -> IO (Maybe RelationalError)
598612executeGraphExpr sessionId (InProcessConnection conf) graphExpr = excMaybe $ do
599613 let strat = ipPersistenceStrategy conf
600614 clientNodes = ipClientNodes conf
601615 sessions = ipSessions conf
602616 graphTvar = ipTransactionGraph conf
617+ mLockFileH = ipLocks conf
618+ lockHandler body = case graphExpr of
619+ Commit -> case mLockFileH of
620+ Nothing -> pure Nothing
621+ Just (lockFileH, lockMVar) -> let acquireLocks = do
622+ lastWrittenDigest <- takeMVar lockMVar
623+ lockFile lockFileH WriteLock
624+ latestDigest <- readGraphTransactionIdDigest strat
625+ pure (latestDigest /= lastWrittenDigest)
626+
627+ releaseLocks _ = do
628+ --still holding the lock- get the latest digest
629+ gDigest <- readGraphTransactionIdDigest strat
630+ unlockFile lockFileH
631+ putMVar lockMVar gDigest
632+ in bracket acquireLocks releaseLocks body
633+ _ -> body False
603634 freshId <- nextRandom
604− manip <- atomically $ do
635+ lockHandler $ \dbWrittenByOtherProcess -> do
636+ --if the database file has been updated since we wrote it last, load it before trying to sync our version done- this can result in TransactionNotAHeadErrors
637+ --read transaction data and compare to existing graph
638+ --in the future, we can detect if updated transaction graph can be safely merged (such as with a transaction on a separate head) (rebase-able commits should force the user to rebase from the client to confirm that the action makes sense)
639+ if dbWrittenByOtherProcess then do
640+ handleGraphFromDiskUpdate sessionId conf
641+ else do
642+ manip <- atomically $ do
643+ eSession <- sessionForSessionId sessionId sessions
644+ --handle graph update by other process
645+ oldGraph <- readTVar graphTvar
646+ case eSession of
647+ Left err -> pure (Left err)
648+ Right session -> do
649+ eGraph <- executeGraphExprSTM_ freshId sessionId sessions graphExpr graphTvar
650+ case eGraph of
651+ Left err -> pure (Left err)
652+ Right newGraph -> do
653+ --handle commmit
654+ if graphExpr == Commit then
655+ case transactionForId (Sess.parentId session) oldGraph of
656+ Left err -> pure $ Left err
657+ Right previousTrans -> do
658+ (evaldNots, nodes) <- executeCommitExprSTM_ (Trans.concreteDatabaseContext previousTrans) (Sess.concreteDatabaseContext session) clientNodes
659+ nodesToNotify <- toList (STMSet.stream nodes)
660+ pure $ Right (evaldNots, nodesToNotify, newGraph)
661+ else
662+ pure $ Right (M.empty, [], newGraph)
663+ case manip of
664+ Left err -> return $ Just err
665+ Right (notsToFire, nodesToNotify, newGraph) -> do
666+ --update filesystem database, if necessary
667+ processTransactionGraphPersistence strat newGraph
668+ sendNotifications nodesToNotify (ipLocalNode conf) notsToFire
669+ pure Nothing
670+executeGraphExpr sessionId conn@(RemoteProcessConnection _) graphExpr = remoteCall conn (ExecuteGraphExpr sessionId graphExpr)
671+
672+-- | Called when the transaction graph on disk has been updated by a secondary process. Reject the commit and update the graph
673+handleGraphFromDiskUpdate :: SessionId -> InProcessConnectionConf -> IO (Maybe RelationalError)
674+handleGraphFromDiskUpdate sessionId conf = do
675+ let graphTVar = ipTransactionGraph conf
676+ strat = ipPersistenceStrategy conf
677+ sessions = ipSessions conf
678+ mScriptSession = ipScriptSession conf
679+ dbdir = case strat of
680+ MinimalPersistence x -> x
681+ CrashSafePersistence x -> x
682+ _ -> error "handleGraphFromDiskUpdate on unpersisted database"
683+ atomically $ do
605684 eSession <- sessionForSessionId sessionId sessions
606− oldGraph <- readTVar graphTvar
607685 case eSession of
608− Left err -> pure (Left err)
686+ Left err -> pure (Just err)
609687 Right session -> do
610− eGraph <- executeGraphExprSTM_ freshId sessionId sessions graphExpr graphTvar
688+ graph <- readTVar graphTVar
689+ let parentTid = Discon.parentId (disconnectedTransaction session)
690+ --this should be re-entrant because it only reads from the disk to create immutable structures
691+ --BUG: here, we unsafely assume that the updated graph is based on the original- that might not be true- we should check that the graph is sound and consistence with the previous graph
692+ eGraph <- unsafeIOToSTM (transactionGraphLoad dbdir graph mScriptSession)
611693 case eGraph of
612− Left err -> pure (Left err)
613− Right newGraph -> do
614− if graphExpr == Commit then
615− case transactionForId (Sess.parentId session) oldGraph of
616− Left err -> pure $ Left err
617− Right previousTrans -> do
618− (evaldNots, nodes) <- executeCommitExprSTM_ (Trans.concreteDatabaseContext previousTrans) (Sess.concreteDatabaseContext session) clientNodes
619− nodesToNotify <- toList (STMSet.stream nodes)
620− pure $ Right (evaldNots, nodesToNotify, newGraph)
621− else
622− pure $ Right (M.empty, [], newGraph)
623− case manip of
624− Left err -> return $ Just err
625− Right (notsToFire, nodesToNotify, newGraph) -> do
626− --update filesystem database, if necessary
627− --this should really grab a lock at the beginning of the method to be threadsafe
628− processPersistence strat newGraph
629− sendNotifications nodesToNotify (ipLocalNode conf) notsToFire
630− return Nothing
631−executeGraphExpr sessionId conn@(RemoteProcessConnection _) graphExpr = remoteCall conn (ExecuteGraphExpr sessionId graphExpr)
632−
633−{-
634−commitSTM_ :: UUID -> SessionId -> Sessions -> TVar TransactionGraph -> STM (Maybe RelationalError)
635−commitSTM_ freshUUID sessionId sessions graph = do
636− mErr <- executeGraphExprSTM_ freshUUID sessionId sessions Commit graph
637− pure $ case mErr of
638− Left err -> Just err
639− Right _ -> Nothing
640−-}
694+ Left err -> pure (Just (DatabaseLoadError err))
695+ Right updatedGraph -> do
696+ writeTVar graphTVar updatedGraph
697+ pure (Just (TransactionIsNotAHeadError parentTid))
641698
642699-- | A trans-graph expression is a relational query executed against the entirety of a transaction graph.
643700executeTransGraphRelationalExpr :: SessionId -> Connection -> TransGraphRelationalExpr -> IO (Either RelationalError Relation)
@@ -686,10 +743,27 @@ rollback :: SessionId -> Connection -> IO (Maybe RelationalError)
686743rollback sessionId conn@(InProcessConnection _) = executeGraphExpr sessionId conn Rollback
687744rollback sessionId conn@(RemoteProcessConnection _) = remoteCall conn (ExecuteGraphExpr sessionId Rollback)
688745
689−processPersistence :: PersistenceStrategy -> TransactionGraph -> IO ()
690−processPersistence NoPersistence _ = return ()
691−processPersistence (MinimalPersistence dbdir) graph = transactionGraphPersist NoDiskSync dbdir graph
692−processPersistence (CrashSafePersistence dbdir) graph = transactionGraphPersist FsyncDiskSync dbdir graph
746+processTransactionGraphPersistence :: PersistenceStrategy -> TransactionGraph -> IO ()
747+processTransactionGraphPersistence NoPersistence _ = pure ()
748+processTransactionGraphPersistence (MinimalPersistence dbdir) graph = transactionGraphPersist NoDiskSync dbdir graph >> pure ()
749+processTransactionGraphPersistence (CrashSafePersistence dbdir) graph = transactionGraphPersist FsyncDiskSync dbdir graph >> pure ()
750+
751+readGraphTransactionIdDigest :: PersistenceStrategy -> IO (Digest SHA256)
752+readGraphTransactionIdDigest NoPersistence = error "attempt to read digest from transaction log without persistence enabled"
753+readGraphTransactionIdDigest (MinimalPersistence dbdir) = readGraphTransactionIdFileDigest dbdir
754+readGraphTransactionIdDigest (CrashSafePersistence dbdir) = readGraphTransactionIdFileDigest dbdir
755+
756+{-
757+sameGraphTransactionIdDigest :: PersistenceStrategy -> Digest SHA256 -> IO (Bool)
758+sameGraphTransactionIdDigest NoPersistence _ = pure True
759+sameGraphTransactionIdDigest persist lastDigest = do
760+ let dbdir = case persist of
761+ MinimalPersistence x -> x
762+ CrashSafePersistence x -> x
763+ newDigest <- readGraphTransactionIdDigest persist
764+ pure (lastDigest == newDigest)
765+
766+-}
693767
694768-- | Return a relation whose type would match that of the relational expression if it were executed. This is useful for checking types and validating a relational expression's types.
695769typeForRelationalExpr :: SessionId -> Connection -> RelationalExpr -> IO (Either RelationalError Relation)
src/lib/ProjectM36/DataTypes/DateTime.hs−1
@@ -3,7 +3,6 @@ import ProjectM36.Base
33import ProjectM36.AtomFunctionBody
44import qualified Data.HashSet as HS
55import Data.Time.Clock.POSIX
6−import Data.Time.Clock
76
87dateTimeAtomFunctions :: AtomFunctions
98dateTimeAtomFunctions = HS.fromList [ AtomFunction {
src/lib/ProjectM36/Error.hs+2−1
@@ -81,6 +81,7 @@ data RelationalError = NoSuchAttributeNamesError (S.Set AttributeName)
8181 | MergeTransactionError MergeError
8282 | ScriptError ScriptCompilationError
8383 | DatabaseContextFunctionUserError DatabaseContextFunctionError
84+ | DatabaseLoadError PersistenceError
8485
8586 | SubschemaNameInUseError SchemaName
8687 | SubschemaNameNotInUseError SchemaName
@@ -94,7 +95,7 @@ data RelationalError = NoSuchAttributeNamesError (S.Set AttributeName)
9495
9596data PersistenceError = InvalidDirectoryError FilePath |
9697 MissingTransactionError TransactionId
97− deriving (Show, Eq, Generic)
98+ deriving (Show, Eq, Generic, Binary, NFData)
9899
99100--collapse list of errors into normal error- if there is just one, just return one
100101someErrors :: [RelationalError] -> RelationalError
src/lib/ProjectM36/FileLock.hsnew file+36
@@ -0,0 +1,36 @@
1+--cross-platform file locking utilizing POSIX file locking on Unix/Linux and Windows file locking
2+--hackage's System.FileLock doesn't support POSIX advisory locks nor locking file based on file descriptors, hence this needless rewrite
3+module ProjectM36.FileLock where
4+
5+#if defined(linux_HOST_OS)
6+import qualified System.Posix.IO as P
7+import System.IO
8+
9+lockStruct :: P.LockRequest -> P.FileLock
10+lockStruct req = (req, AbsoluteSeek, 0, 0)
11+
12+--blocks on lock, if necessary
13+lockFile :: Handle -> LockType -> IO ()
14+lockFile file lock = do
15+ fd <- P.handleToFd file
16+ let lockt = case lock of
17+ WriteLock -> P.WriteLock
18+ ReadLock -> P.ReadLock
19+ P.waitToSetLock fd (lockStruct lockt)
20+
21+unlockFile :: Handle -> IO ()
22+unlockFile file = do
23+ fd <- P.handleToFd file
24+ P.waitToSetLock fd (lockStruct P.Unlock)
25+#endif
26+
27+data LockType = ReadLock | WriteLock
28+
29+{-
30+lockFileSTM :: Handle -> LockType -> STM ()
31+lockFileSTM file lock = unsafeIOToSTM $ onException (lockFile file lock) (unlockFile file)
32+
33+unlockFileSTM :: Handle -> STM ()
34+unlockFileSTM file = unsafeIOToSTM $ unlockFile file
35+-}
36+
src/lib/ProjectM36/Persist.hs+5−4
@@ -5,7 +5,8 @@ module ProjectM36.Persist (writeFileSync,
55 renameSync,
66 DiskSync(..)) where
77-- on Windows, use FlushFileBuffers and MoveFileEx
8−
8+import qualified Data.Text.IO as TIO
9+import Data.Text
910#if defined(mingw32_HOST_OS)
1011import qualified System.Win32 as Win32
1112#else
@@ -19,7 +20,7 @@ import System.Posix.IO (handleToFd)
1920import Foreign.C
2021#endif
2122
22−import System.IO (withFile, IOMode(WriteMode), hPutStr, Handle)
23+import System.IO (withFile, IOMode(WriteMode), Handle)
2324import qualified Data.ByteString.Lazy as BS
2425
2526#if defined(mingw32_HOST_OS)
@@ -30,11 +31,11 @@ foreign import ccall unsafe "cDirectoryFsync" cHSDirectoryFsync :: CString -> IO
3031
3132data DiskSync = NoDiskSync | FsyncDiskSync
3233
33−writeFileSync :: DiskSync -> FilePath -> String -> IO()
34+writeFileSync :: DiskSync -> FilePath -> Text -> IO()
3435writeFileSync sync path strOut = withFile path WriteMode handler
3536 where
3637 handler handle = do
37− hPutStr handle strOut
38+ TIO.hPutStr handle strOut
3839 syncHandle sync handle
3940
4041renameSync :: DiskSync -> FilePath -> FilePath -> IO ()
src/lib/ProjectM36/Transaction/Persist.hs+1−3
@@ -19,8 +19,6 @@ import Control.Exception
1919import GHC
2020import GHC.Paths
2121
22−import Debug.Trace
23−
2422getDirectoryNames :: FilePath -> IO [FilePath]
2523getDirectoryNames path = do
2624 subpaths <- getDirectoryContents path
@@ -164,7 +162,7 @@ writeDBCFuncs sync transDir funcs = mapM_ (writeDBCFunc sync transDir) (HS.toLis
164162writeDBCFunc :: DiskSync -> FilePath -> DatabaseContextFunction -> IO ()
165163writeDBCFunc sync transDir func = do
166164 let dbcFuncPath = dbcFuncsDir transDir </> T.unpack (dbcFuncName func)
167− writeBSFileSync sync (traceShowId dbcFuncPath) (B.encode (dbcFuncType func, databaseContextFunctionScript func))
165+ writeBSFileSync sync dbcFuncPath (B.encode (dbcFuncType func, databaseContextFunctionScript func))
168166
169167readDBCFuncs :: FilePath -> Maybe ScriptSession -> IO DatabaseContextFunctions
170168readDBCFuncs transDir mScriptSession = do
src/lib/ProjectM36/TransactionGraph.hs+4−4
@@ -17,7 +17,6 @@ import GHC.Generics
1717import Data.Binary
1818import ProjectM36.TransactionGraph.Merge
1919import Data.Either (lefts, rights, isRight)
20−--import Debug.Trace
2120
2221-- | Record a lookup for a specific transaction in the graph.
2322data TransactionIdLookup = TransactionIdLookup TransactionId |
@@ -55,7 +54,10 @@ emptyTransactionGraph :: TransactionGraph
5554emptyTransactionGraph = TransactionGraph M.empty S.empty
5655
5756transactionForHead :: HeadName -> TransactionGraph -> Maybe Transaction
58−transactionForHead headName (TransactionGraph heads _) = M.lookup headName heads
57+transactionForHead headName graph = M.lookup headName (transactionHeadsForGraph graph)
58+
59+headList :: TransactionGraph -> [(HeadName, TransactionId)]
60+headList graph = map (\(k,v) -> (k, transactionId v)) (M.assocs (transactionHeadsForGraph graph))
5961
6062headNameForTransaction :: Transaction -> TransactionGraph -> Maybe HeadName
6163headNameForTransaction transaction (TransactionGraph heads _) = if M.null matchingTrans then
@@ -186,7 +188,6 @@ walkChildTransactions seenTransSet graph trans =
186188-- the current transaction is not part of the transaction graph until it is committed
187189evalGraphOp :: TransactionId -> DisconnectedTransaction -> TransactionGraph -> TransactionGraphOperator -> Either RelationalError (DisconnectedTransaction, TransactionGraph)
188190
189−--affects only disconncted transaction
190191evalGraphOp _ _ graph (JumpToTransaction jumpId) = case transactionForId jumpId graph of
191192 Left err -> Left err
192193 Right parentTrans -> Right (newTrans, graph)
@@ -194,7 +195,6 @@ evalGraphOp _ _ graph (JumpToTransaction jumpId) = case transactionForId jumpId
194195 newTrans = DisconnectedTransaction jumpId (schemas parentTrans)
195196
196197-- switch from one head to another
197−-- affects only disconnectedtransaction
198198evalGraphOp _ _ graph (JumpToHead headName) =
199199 case transactionForHead headName graph of
200200 Just newHeadTransaction -> let disconnectedTrans = DisconnectedTransaction (transactionId newHeadTransaction) (schemas newHeadTransaction) in
src/lib/ProjectM36/TransactionGraph/Persist.hs+57−27
@@ -6,17 +6,23 @@ import ProjectM36.Transaction.Persist
66import ProjectM36.Base
77import ProjectM36.ScriptSession
88import ProjectM36.Persist (writeFileSync, renameSync, DiskSync)
9+import ProjectM36.FileLock
910import System.Directory
1011import System.FilePath
1112import System.IO.Temp
12−import Data.List
13+import System.IO
1314import qualified Data.UUID as U
1415import qualified Data.Set as S
1516import qualified Data.Map as M
1617import qualified Data.Text as T
18+import Data.Text.Encoding
1719import Control.Monad (foldM)
1820import Data.Either (isRight)
1921import Data.Maybe (catMaybes)
22+import Control.Exception.Base
23+import Crypto.Hash
24+import qualified Data.Text.IO as TIO
25+import Data.Monoid
2026
2127{-
2228The "m36v1" 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)
@@ -31,44 +37,63 @@ transactionLogPath dbdir = dbdir </> "m36v1"
3137headsPath :: FilePath -> FilePath
3238headsPath dbdir = dbdir </> "heads"
3339
40+lockFilePath :: FilePath -> FilePath
41+lockFilePath dbdir = dbdir </> "lockFile"
42+
3443{-
3544verify that the database directory is valid or bootstrap it
3645-note: checking for the existence of every transaction may be prohibitively expensive
46+- return error or lock file handle which is already locked with a read lock
3747-}
3848
39−setupDatabaseDir :: DiskSync -> FilePath -> TransactionGraph -> IO (Maybe PersistenceError)
49+setupDatabaseDir :: DiskSync -> FilePath -> TransactionGraph -> IO (Either PersistenceError (Handle, Digest SHA256))
4050setupDatabaseDir sync dbdir bootstrapGraph = do
4151 dbdirExists <- doesDirectoryExist dbdir
4252 m36exists <- doesFileExist (transactionLogPath dbdir)
43− if dbdirExists && m36exists then
44− return Nothing
53+ if dbdirExists && m36exists then do
54+ --no directories to write, just
55+ lockFileH <- openLockFile dbdir
56+ gDigest <- bracket_ (lockFile lockFileH WriteLock) (unlockFile lockFileH) (readGraphTransactionIdFileDigest dbdir)
57+ pure (Right (lockFileH, gDigest))
4558 else if not m36exists then do
46− bootstrapDatabaseDir sync dbdir bootstrapGraph
47− return Nothing
59+ locks <- bootstrapDatabaseDir sync dbdir bootstrapGraph
60+ pure (Right locks)
4861 else
49− return $ Just (InvalidDirectoryError dbdir)
50−
62+ pure (Left (InvalidDirectoryError dbdir))
5163{-
52−initialize a database directory with the graph from which to bootstrap
64+initialize a database directory with the graph from which to bootstrap- return lock file handle
5365-}
54−bootstrapDatabaseDir :: DiskSync -> FilePath -> TransactionGraph -> IO ()
66+bootstrapDatabaseDir :: DiskSync -> FilePath -> TransactionGraph -> IO (Handle, Digest SHA256)
5567bootstrapDatabaseDir sync dbdir bootstrapGraph = do
5668 createDirectory dbdir
57− transactionGraphPersist sync dbdir bootstrapGraph
69+ lockFileH <- openLockFile dbdir
70+ digest <- bracket_ (lockFile lockFileH WriteLock) (unlockFile lockFileH) (transactionGraphPersist sync dbdir bootstrapGraph)
71+ pure (lockFileH, digest)
72+
73+openLockFile :: FilePath -> IO (Handle)
74+openLockFile dbdir = do
75+ lockFileH <- openFile (lockFilePath dbdir) WriteMode
76+ pure lockFileH
5877
5978{-
6079incrementally updates an existing database directory
6180--algorithm:
62−-check that all heads have been written
81+--assume that all transaction data has already been written
6382-assume that all non-head transactions have already been written because this is an incremental (and concurrent!) write method
6483--store the head names with a symlink to the transaction under "heads"
6584-}
66−transactionGraphPersist :: DiskSync -> FilePath -> TransactionGraph -> IO ()
85+transactionGraphPersist :: DiskSync -> FilePath -> TransactionGraph -> IO (Digest SHA256)
6786transactionGraphPersist sync destDirectory graph = do
68− mapM_ (writeTransaction sync destDirectory) $ M.elems (transactionHeadsForGraph graph)
69− writeGraphTransactionIdFile sync destDirectory graph
87+ transactionHeadTransactionsPersist sync destDirectory graph
88+ --write graph file
89+ newDigest <- writeGraphTransactionIdFile sync destDirectory graph
90+ --write heads file
7091 transactionGraphHeadsPersist sync destDirectory graph
71− return ()
92+ pure newDigest
93+
94+-- | The incremental writer which only writes from the set of heads. New heads must be written on every commit. Most heads will already be written on every commit.
95+transactionHeadTransactionsPersist :: DiskSync -> FilePath -> TransactionGraph -> IO ()
96+transactionHeadTransactionsPersist sync destDirectory graphIn = mapM_ (writeTransaction sync destDirectory) $ M.elems (transactionHeadsForGraph graphIn)
7297
7398{-
7499write graph heads to a file which can be atomically swapped
@@ -76,12 +101,12 @@ write graph heads to a file which can be atomically swapped
76101--writing the heads in a directory is a synchronization nightmare, so just write the binary to a file and swap atomically
77102transactionGraphHeadsPersist :: DiskSync -> FilePath -> TransactionGraph -> IO ()
78103transactionGraphHeadsPersist sync dbdir graph = do
79− let headFileStr :: (HeadName, Transaction) -> String
80− headFileStr (headName, trans) = T.unpack headName ++ " " ++ U.toString (transactionId trans)
104+ let headFileStr :: (HeadName, Transaction) -> T.Text
105+ headFileStr (headName, trans) = headName <> " " <> U.toText (transactionId trans)
81106 withTempDirectory dbdir ".heads.tmp" $ \tempHeadsDir -> do
82107 let tempHeadsPath = tempHeadsDir </> "heads"
83108 headsStrLines = map headFileStr $ M.toList (transactionHeadsForGraph graph)
84− writeFileSync sync tempHeadsPath $ intercalate "\n" headsStrLines
109+ writeFileSync sync tempHeadsPath $ T.intercalate "\n" headsStrLines
85110 renameSync sync tempHeadsPath (headsPath dbdir)
86111
87112transactionGraphHeadsLoad :: FilePath -> IO [(HeadName,TransactionId)]
@@ -96,7 +121,6 @@ transactionGraphHeadsLoad dbdir = do
96121load any transactions which are not already part of the incoming transaction graph
97122-}
98123
99−--ALERT I need to figure out how to manage the transaction heads (branch names)
100124transactionGraphLoad :: FilePath -> TransactionGraph -> Maybe ScriptSession -> IO (Either PersistenceError TransactionGraph)
101125transactionGraphLoad dbdir graphIn mScriptSession = do
102126 --optimization: perform tail-bisection search to find last-recorded transaction in the existing stream- replay the rest
@@ -131,20 +155,26 @@ readTransactionIfNecessary dbdir transId mScriptSession graphIn = do
131155 Left err -> return $ Left err
132156 Right trans' -> return $ Right $ TransactionGraph (transactionHeadsForGraph graphIn) (S.insert trans' (transactionsForGraph graphIn))
133157
134−writeGraphTransactionIdFile :: DiskSync -> FilePath -> TransactionGraph -> IO ()
135−writeGraphTransactionIdFile sync destDirectory (TransactionGraph _ transSet) = writeFileSync sync graphFile uuidInfo
158+writeGraphTransactionIdFile :: DiskSync -> FilePath -> TransactionGraph -> IO (Digest SHA256)
159+writeGraphTransactionIdFile sync destDirectory (TransactionGraph _ transSet) = writeFileSync sync graphFile uuidInfo >> pure digest
136160 where
137161 graphFile = destDirectory </> "m36v1"
138− uuidInfo = intercalate "\n" graphLines
162+ uuidInfo = T.intercalate "\n" graphLines
163+ digest = hashWith SHA256 (encodeUtf8 uuidInfo)
139164 graphLines = S.toList $ S.map graphLine transSet
140− graphLine trans = U.toString (transactionId trans) ++ " " ++ intercalate " " (S.toList (S.map U.toString $ transactionParentIds trans))
165+ graphLine trans = U.toText (transactionId trans) <> " " <> T.intercalate " " (S.toList (S.map U.toText $ transactionParentIds trans))
166+
167+readGraphTransactionIdFileDigest :: FilePath -> IO (Digest SHA256)
168+readGraphTransactionIdFileDigest dbdir = do
169+ graphTransactionIdData <- TIO.readFile (transactionLogPath dbdir)
170+ pure (hashWith SHA256 (encodeUtf8 graphTransactionIdData))
141171
142172readGraphTransactionIdFile :: FilePath -> IO (Either PersistenceError [(TransactionId, [TransactionId])])
143173readGraphTransactionIdFile dbdir = do
144174 --read in all transactions' uuids
145− let grapher line = let tids = catMaybes (map U.fromString (words line)) in
175+ let grapher line = let tids = catMaybes (map U.fromText (T.words line)) in
146176 (head tids, tail tids)
147177 --warning: uses lazy IO
148− graphTransactionIdData <- readFile (transactionLogPath dbdir)
149− return $ Right (map grapher $ lines graphTransactionIdData)
178+ graphTransactionIdData <- TIO.readFile (transactionLogPath dbdir)
179+ return $ Right (map grapher $ T.lines graphTransactionIdData)
150180