@@ -20,7 +20,7 @@ module ProjectM36.Client
20 typeForRelationalExpr,
20 typeForRelationalExpr,
21 inclusionDependencies,
21 inclusionDependencies,
22 planForDatabaseContextExpr,
22 planForDatabaseContextExpr,
23− processPersistence,
23+ processTransactionGraphPersistence,
24 currentSchemaName,
24 currentSchemaName,
25 SchemaName,
25 SchemaName,
@@ -101,6 +101,7 @@ import ProjectM36.TransactionGraph.Persist
101import ProjectM36.Attribute hiding (atomTypes)
101import ProjectM36.Attribute hiding (atomTypes)
102import ProjectM36.TransGraphRelationalExpression (TransGraphRelationalExpr, evalTransGraphRelationalExpr)
102import ProjectM36.TransGraphRelationalExpression (TransGraphRelationalExpr, evalTransGraphRelationalExpr)
103import ProjectM36.Persist (DiskSync(..))
103import ProjectM36.Persist (DiskSync(..))
104+import ProjectM36.FileLock
104import ProjectM36.Notifications
105import ProjectM36.Notifications
105import ProjectM36.Server.RemoteCallTypes
106import ProjectM36.Server.RemoteCallTypes
106import qualified ProjectM36.DisconnectedTransaction as Discon
107import qualified ProjectM36.DisconnectedTransaction as Discon
@@ -108,6 +109,8 @@ import ProjectM36.Relation (typesAsRelation)
108import ProjectM36.ScriptSession (initScriptSession, ScriptSession)
109import ProjectM36.ScriptSession (initScriptSession, ScriptSession)
109import qualified ProjectM36.Relation as R
110import qualified ProjectM36.Relation as R
110import qualified ProjectM36.DatabaseContext as DBC
111import qualified ProjectM36.DatabaseContext as DBC
112+import Control.Exception.Base
112import Network.Transport (Transport(closeTransport))
115import Network.Transport (Transport(closeTransport))
113import Network.Transport.TCP (createTransport, defaultTCPParameters, encodeEndPointAddress)
116import Network.Transport.TCP (createTransport, defaultTCPParameters, encodeEndPointAddress)
@@ -117,7 +120,6 @@ import Control.Distributed.Process.ManagedProcess.Client (call, safeCall)
117import Control.Distributed.Process (NodeId(..), reconnect)
120import Control.Distributed.Process (NodeId(..), reconnect)
119import Data.UUID.V4 (nextRandom)
122import Data.UUID.V4 (nextRandom)
120−import Control.Concurrent.STM
121import Data.Word
123import Data.Word
122import Control.Distributed.Process (ProcessId, Process, receiveWait, send, match)
124import Control.Distributed.Process (ProcessId, Process, receiveWait, send, match)
123import Control.Exception (IOException, handle, AsyncException, throwIO, fromException, Exception)
125import Control.Exception (IOException, handle, AsyncException, throwIO, fromException, Exception)
@@ -134,6 +136,7 @@ import Data.Binary (Binary)
134import GHC.Generics (Generic)
136import GHC.Generics (Generic)
135import Control.DeepSeq (force)
137import Control.DeepSeq (force)
136import System.IO
138import System.IO
138type Hostname = String
141type Hostname = String
@@ -195,6 +198,9 @@ defaultRemoteConnectionInfo = RemoteProcessConnectionInfo defaultDatabaseName (c
195-- | The 'Connection' represents either local or remote access to a database. All operations flow through the connection.
198-- | The 'Connection' represents either local or remote access to a database. All operations flow through the connection.
196type ClientNodes = STMSet.Set ProcessId
199type ClientNodes = STMSet.Set ProcessId
201+type TransactionGraphLockHandle = Handle
202+type LockFileHash = Digest SHA256
198-- internal structure specific to in-process connections
204-- internal structure specific to in-process connections
199data InProcessConnectionConf = InProcessConnectionConf {
205data InProcessConnectionConf = InProcessConnectionConf {
200 ipPersistenceStrategy :: PersistenceStrategy,
206 ipPersistenceStrategy :: PersistenceStrategy,
@@ -203,7 +209,8 @@ data InProcessConnectionConf = InProcessConnectionConf {
203 ipTransactionGraph :: TVar TransactionGraph,
209 ipTransactionGraph :: TVar TransactionGraph,
204 ipScriptSession :: Maybe ScriptSession,
210 ipScriptSession :: Maybe ScriptSession,
205 ipLocalNode :: LocalNode,
211 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
209data RemoteProcessConnectionConf = RemoteProcessConnectionConf {
216data RemoteProcessConnectionConf = RemoteProcessConnectionConf {
@@ -275,14 +282,16 @@ connectProjectM36 (InProcessConnectionInfo strat notificationCallback ghcPkgPath
275 (localNode, transport) <- createLocalNode
282 (localNode, transport) <- createLocalNode
276 notificationPid <- startNotificationListener localNode notificationCallback
283 notificationPid <- startNotificationListener localNode notificationCallback
277 mScriptSession <- createScriptSession ghcPkgPaths
284 mScriptSession <- createScriptSession ghcPkgPaths
278 let conn = InProcessConnection (InProcessConnectionConf {
286 let conn = InProcessConnection (InProcessConnectionConf {
279− ipPersistenceStrategy =strat,
287+ ipPersistenceStrategy = strat,
280 ipClientNodes = clientNodes,
288 ipClientNodes = clientNodes,
281 ipSessions = sessions,
289 ipSessions = sessions,
282 ipTransactionGraph = graphTvar,
290 ipTransactionGraph = graphTvar,
283 ipScriptSession = mScriptSession,
291 ipScriptSession = mScriptSession,
284 ipLocalNode = localNode,
292 ipLocalNode = localNode,
285− ipTransport = transport })
293+ ipTransport = transport,
286 addClientNode conn notificationPid
295 addClientNode conn notificationPid
287 pure (Right conn)
296 pure (Right conn)
288 MinimalPersistence dbdir -> connectPersistentProjectM36 strat NoDiskSync dbdir freshGraph notificationCallback ghcPkgPaths
297 MinimalPersistence dbdir -> connectPersistentProjectM36 strat NoDiskSync dbdir freshGraph notificationCallback ghcPkgPaths
@@ -318,8 +327,8 @@ connectPersistentProjectM36 :: PersistenceStrategy ->
318connectPersistentProjectM36 strat sync dbdir freshGraph notificationCallback ghcPkgPaths = do
327connectPersistentProjectM36 strat sync dbdir freshGraph notificationCallback ghcPkgPaths = do
319 err <- setupDatabaseDir sync dbdir freshGraph
328 err <- setupDatabaseDir sync dbdir freshGraph
320 case err of
329 case err of
321− Just err' -> return $ Left (SetupDatabaseDirectoryError err')
330+ Left err' -> return $ Left (SetupDatabaseDirectoryError err')
322− Nothing -> do
331+ Right (lockFileH, digest) -> do
323 mScriptSession <- createScriptSession ghcPkgPaths
332 mScriptSession <- createScriptSession ghcPkgPaths
324 graph <- transactionGraphLoad dbdir emptyTransactionGraph mScriptSession
333 graph <- transactionGraphLoad dbdir emptyTransactionGraph mScriptSession
325 case graph of
334 case graph of
@@ -329,6 +338,7 @@ connectPersistentProjectM36 strat sync dbdir freshGraph notificationCallback ghc
329 sessions <- STMMap.newIO
338 sessions <- STMMap.newIO
330 clientNodes <- STMSet.newIO
339 clientNodes <- STMSet.newIO
331 (localNode, transport) <- createLocalNode
340 (localNode, transport) <- createLocalNode
341+ lockMVar <- newMVar digest
332 let conn = InProcessConnection (InProcessConnectionConf {
342 let conn = InProcessConnection (InProcessConnectionConf {
333 ipPersistenceStrategy = strat,
343 ipPersistenceStrategy = strat,
334 ipClientNodes = clientNodes,
344 ipClientNodes = clientNodes,
@@ -336,7 +346,8 @@ connectPersistentProjectM36 strat sync dbdir freshGraph notificationCallback ghc
336 ipTransactionGraph = tvarGraph,
346 ipTransactionGraph = tvarGraph,
337 ipScriptSession = mScriptSession,
347 ipScriptSession = mScriptSession,
338 ipLocalNode = localNode,
348 ipLocalNode = localNode,
339− ipTransport = transport
349+ ipTransport = transport,
350+ ipLocks = Just (lockFileH, lockMVar)
342 notificationPid <- startNotificationListener localNode notificationCallback
353 notificationPid <- startNotificationListener localNode notificationCallback
@@ -578,6 +589,7 @@ executeGraphExprSTM_ freshId sessionId sessions graphExpr graphTvar = do
578 Right session -> case evalGraphOp freshId (Sess.disconnectedTransaction session) graph graphExpr of
589 Right session -> case evalGraphOp freshId (Sess.disconnectedTransaction session) graph graphExpr of
579 Left err -> pure $ Left err
590 Left err -> pure $ Left err
580 Right (discon', graph') -> do
591 Right (discon', graph') -> do
581 writeTVar graphTvar graph'
593 writeTVar graphTvar graph'
582 let newSession = Session discon' (Sess.schemaName session)
594 let newSession = Session discon' (Sess.schemaName session)
583 STMMap.insert newSession sessionId sessions
595 STMMap.insert newSession sessionId sessions
@@ -591,53 +603,98 @@ executeCommitExprSTM_ oldContext newContext nodes = do
591 evaldNots = M.map mkEvaldNot fireNots
603 evaldNots = M.map mkEvaldNot fireNots
592 mkEvaldNot notif = EvaluatedNotification { notification = notif, reportRelation = evalState (RE.evalRelationalExpr (reportExpr notif)) (RE.mkRelationalExprState oldContext) }
604 mkEvaldNot notif = EvaluatedNotification { notification = notif, reportRelation = evalState (RE.evalRelationalExpr (reportExpr notif)) (RE.mkRelationalExprState oldContext) }
593 pure (evaldNots, nodes)
605 pure (evaldNots, nodes)
596-- | Execute a transaction graph expression in the context of the session and connection. Transaction graph operators modify the transaction graph state.
607-- | 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
597executeGraphExpr :: SessionId -> Connection -> TransactionGraphOperator -> IO (Maybe RelationalError)
611executeGraphExpr :: SessionId -> Connection -> TransactionGraphOperator -> IO (Maybe RelationalError)
598executeGraphExpr sessionId (InProcessConnection conf) graphExpr = excMaybe $ do
612executeGraphExpr sessionId (InProcessConnection conf) graphExpr = excMaybe $ do
599 let strat = ipPersistenceStrategy conf
613 let strat = ipPersistenceStrategy conf
600 clientNodes = ipClientNodes conf
614 clientNodes = ipClientNodes conf
601 sessions = ipSessions conf
615 sessions = ipSessions conf
602 graphTvar = ipTransactionGraph conf
616 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)
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
603 freshId <- nextRandom
634 freshId <- nextRandom
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
604− manip <- atomically $ do
642+ manip <- atomically $ do
643+ eSession <- sessionForSessionId sessionId sessions
644+ --handle graph update by other process
645+ oldGraph <- readTVar graphTvar
647+ Left err -> pure (Left err)
649+ eGraph <- executeGraphExprSTM_ freshId sessionId sessions graphExpr graphTvar
651+ Left err -> pure (Left err)
652+ Right newGraph -> do
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)
662+ pure $ Right (M.empty, [], newGraph)
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
670+executeGraphExpr sessionId conn@(RemoteProcessConnection _) graphExpr = remoteCall conn (ExecuteGraphExpr sessionId graphExpr)
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"
605 eSession <- sessionForSessionId sessionId sessions
684 eSession <- sessionForSessionId sessionId sessions
606− oldGraph <- readTVar graphTvar
607 case eSession of
685 case eSession of
608− Left err -> pure (Left err)
686+ Left err -> pure (Just err)
609 Right session -> do
687 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)
611 case eGraph of
693 case eGraph of
612− Left err -> pure (Left err)
694+ Left err -> pure (Just (DatabaseLoadError err))
613− Right newGraph -> do
695+ Right updatedGraph -> do
614− if graphExpr == Commit then
696+ writeTVar graphTVar updatedGraph
615− case transactionForId (Sess.parentId session) oldGraph of
697+ pure (Just (TransactionIsNotAHeadError parentTid))
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)
622− pure $ Right (M.empty, [], newGraph)
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
631−executeGraphExpr sessionId conn@(RemoteProcessConnection _) graphExpr = remoteCall conn (ExecuteGraphExpr sessionId graphExpr)
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
638− Left err -> Just err
642-- | A trans-graph expression is a relational query executed against the entirety of a transaction graph.
699-- | A trans-graph expression is a relational query executed against the entirety of a transaction graph.
643executeTransGraphRelationalExpr :: SessionId -> Connection -> TransGraphRelationalExpr -> IO (Either RelationalError Relation)
700executeTransGraphRelationalExpr :: SessionId -> Connection -> TransGraphRelationalExpr -> IO (Either RelationalError Relation)
@@ -686,10 +743,27 @@ rollback :: SessionId -> Connection -> IO (Maybe RelationalError)
686rollback sessionId conn@(InProcessConnection _) = executeGraphExpr sessionId conn Rollback
743rollback sessionId conn@(InProcessConnection _) = executeGraphExpr sessionId conn Rollback
687rollback sessionId conn@(RemoteProcessConnection _) = remoteCall conn (ExecuteGraphExpr sessionId Rollback)
744rollback sessionId conn@(RemoteProcessConnection _) = remoteCall conn (ExecuteGraphExpr sessionId Rollback)
689−processPersistence :: PersistenceStrategy -> TransactionGraph -> IO ()
746+processTransactionGraphPersistence :: PersistenceStrategy -> TransactionGraph -> IO ()
690−processPersistence NoPersistence _ = return ()
747+processTransactionGraphPersistence NoPersistence _ = pure ()
691−processPersistence (MinimalPersistence dbdir) graph = transactionGraphPersist NoDiskSync dbdir graph
748+processTransactionGraphPersistence (MinimalPersistence dbdir) graph = transactionGraphPersist NoDiskSync dbdir graph >> pure ()
692−processPersistence (CrashSafePersistence dbdir) graph = transactionGraphPersist FsyncDiskSync dbdir graph
749+processTransactionGraphPersistence (CrashSafePersistence dbdir) graph = transactionGraphPersist FsyncDiskSync dbdir graph >> pure ()
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
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)
694-- | 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.
768-- | 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.
695typeForRelationalExpr :: SessionId -> Connection -> RelationalExpr -> IO (Either RelationalError Relation)
769typeForRelationalExpr :: SessionId -> Connection -> RelationalExpr -> IO (Either RelationalError Relation)