on master help sign in
handle exceptions in AtomFunctionBody scripts
commit4db131238fd7d5aa2220520e4dedefdc95d8b026 authorAgentM <agentm@themactionfaction.com> dateWed 24 Aug 2016, 11:59 (10 years ago) parentc0dfe20
5 files changed+112−28
project-m36.cabal+1−1 src/bin/TutorialD/Interpreter/Base.hs+46−8 src/lib/ProjectM36/Client.hs+33−15 src/lib/ProjectM36/Error.hs+1 test/TutorialD/Interpreter/AtomFunctionScript.hs+31−4
project-m36.cabal+1−1
@@ -272,7 +272,7 @@ Test-Suite test-tutoriald
272272 Hs-Source-Dirs: ./src/bin, ., test
273273 CPP-Options: -DPROJECTM36_VERSION="v0.1"
274274
275−Test-Suite test-atomfunctionbody
275+Test-Suite test-tutoriald-atomfunctionscript
276276 Default-Language: Haskell2010
277277 Default-Extensions: OverloadedStrings
278278 type: exitcode-stdio-1.0
src/bin/TutorialD/Interpreter/Base.hs+46−8
@@ -14,6 +14,14 @@ import System.IO
1414import ProjectM36.Relation.Show.Term
1515import GHC.Generics
1616
17+displayOpResult :: TutorialDOperatorResult -> IO ()
18+displayOpResult QuitResult = return ()
19+displayOpResult (DisplayResult out) = TIO.putStrLn out
20+displayOpResult (DisplayIOResult ioout) = ioout
21+displayOpResult (DisplayErrorResult err) = TIO.hPutStrLn stderr ("ERR: " `T.append` err)
22+displayOpResult QuietSuccessResult = return ()
23+displayOpResult (DisplayRelationResult rel) = TIO.putStrLn (showRelation rel)
24+
1725lexer :: Token.TokenParser ()
1826lexer = Token.makeTokenParser tutD
1927 where tutD = emptyDef {
@@ -47,6 +55,12 @@ comma = Token.comma lexer
4755pipe :: Parser String
4856pipe = Token.symbol lexer "|"
4957
58+quote :: Parser String
59+quote = Token.symbol lexer "\""
60+
61+tripleQuote :: Parser String
62+tripleQuote = Token.symbol lexer "\"\"\""
63+
5064arrow :: Parser String
5165arrow = Token.symbol lexer "->"
5266
@@ -92,18 +106,42 @@ data TutorialDOperatorResult = QuitResult |
92106
93107type TransactionGraphWasUpdated = Bool
94108
95−displayOpResult :: TutorialDOperatorResult -> IO ()
96−displayOpResult QuitResult = return ()
97−displayOpResult (DisplayResult out) = TIO.putStrLn out
98−displayOpResult (DisplayIOResult ioout) = ioout
99−displayOpResult (DisplayErrorResult err) = TIO.hPutStrLn stderr ("ERR: " `T.append` err)
100−displayOpResult QuietSuccessResult = return ()
101−displayOpResult (DisplayRelationResult rel) = TIO.putStrLn (showRelation rel)
102−
109+{-
103110quotedChar :: Parser Char
104111quotedChar = noneOf "\""
105112 <|> try (string "\"\"" >> return '"')
106113
107114quotedString :: Parser String
108115quotedString = string "\"" *> many quotedChar <* string "\"" <* whiteSpace
116+-}
117+escapedString :: Parser String
118+escapedString = do
119+ _ <- char '\\'
120+ let lookupAssoc = [('n',"\n"), ('\"',"\""), ('\\', "\\")]
121+ c <- oneOf "\\\"n" -- all the characters which can be escaped
122+ case lookup c lookupAssoc of
123+ Just v -> pure v
124+ Nothing -> fail "failed to handle escaped character"
125+
126+--http://stackoverflow.com/questions/24106314/parser-for-quoted-string-using-parsec
127+nonEscapedChar :: Parser Char
128+nonEscapedChar = noneOf "\\\""
129+
130+character :: Parser String
131+character = fmap return nonEscapedChar <|> escapedString
132+
133+--allow for python-style triple quoting because guessing the correct amount of escapes in different contexts is annoying
134+tripleQuotedString :: Parser String
135+tripleQuotedString = do
136+ _ <- tripleQuote
137+ manyTill anyChar (try (tripleQuote >> notFollowedBy quote))
138+
139+normalQuotedString :: Parser String
140+normalQuotedString = do
141+ _ <- quote
142+ strings <- many character
143+ _ <- quote
144+ pure (concat strings)
109145
146+quotedString :: Parser String
147+quotedString = try tripleQuotedString <|> normalQuotedString
src/lib/ProjectM36/Client.hs+33−15
@@ -1,4 +1,4 @@
1−{-# LANGUAGE DeriveAnyClass, DeriveGeneric #-}
1+{-# LANGUAGE DeriveAnyClass, DeriveGeneric, ScopedTypeVariables #-}
22module ProjectM36.Client
33 (ConnectionInfo(..),
44 Connection(..),
@@ -76,7 +76,7 @@ import Data.UUID.V4 (nextRandom)
7676import Control.Concurrent.STM
7777import Data.Word
7878import Control.Distributed.Process (ProcessId, Process, receiveWait, send, match)
79−import Control.Exception (IOException)
79+import Control.Exception (IOException, SomeException(..), handle)
8080import Control.Concurrent.MVar
8181import qualified Data.Map as M
8282import Control.Distributed.Process.Serializable (Serializable)
@@ -88,6 +88,7 @@ import ProjectM36.Sessions
8888import ListT
8989import Data.Binary (Binary)
9090import GHC.Generics (Generic)
91+import Control.DeepSeq (force)
9192--import Debug.Trace
9293
9394type Hostname = String
@@ -206,7 +207,7 @@ connectProjectM36 (RemoteProcessConnectionInfo databaseName serverNodeId notific
206207 eLocalNode <- commonLocalNode
207208 notificationListenerPid <- startNotificationListener notificationCallback
208209 let dbName = remoteDBLookupName databaseName
209− putStrLn $ show serverNodeId ++ " " ++ dbName
210+ --putStrLn $ show serverNodeId ++ " " ++ dbName
210211 case eLocalNode of
211212 Left err -> pure (Left err)
212213 Right localNode -> do
@@ -301,6 +302,19 @@ close (RemoteProcessConnection localNode serverProcessId) = do
301302 runProcessResult localNode $ do
302303 call serverProcessId Logout
303304
305+--within the database server, we must catch and handle all exception lest they take down the database process- this handling might be different for other use-cases
306+--exceptions should generally *NOT* be thrown from any Project:M36 code paths, but third-party code such as AtomFunction scripts could conceivably throw undefined, etc.
307+
308+excMaybe :: IO (Maybe RelationalError) -> IO (Maybe RelationalError)
309+excMaybe m = handle handler m
310+ where
311+ handler (exc :: SomeException) = pure (Just (UnhandledExceptionError (show exc)))
312+
313+excEither :: IO (Either RelationalError a) -> IO (Either RelationalError a)
314+excEither m = handle handler m
315+ where
316+ handler (exc :: SomeException) = pure (Left (UnhandledExceptionError (show exc)))
317+
304318runProcessResult :: LocalNode -> Process a -> IO a
305319runProcessResult localNode proc = do
306320 ret <- newEmptyMVar
@@ -324,32 +338,36 @@ sessionForSessionId sessionId sessions = do
324338
325339-- | Execute a relational expression in the context of the session and connection. Relational expressions are queries and therefore cannot alter the database.
326340executeRelationalExpr :: SessionId -> Connection -> RelationalExpr -> IO (Either RelationalError Relation)
327−executeRelationalExpr sessionId (InProcessConnection _ _ sessions _ _) expr = atomically $ do
341+executeRelationalExpr sessionId (InProcessConnection _ _ sessions _ _) expr = excEither $ atomically $ do
328342 eSession <- sessionForSessionId sessionId sessions
329343 case eSession of
330344 Left err -> pure $ Left err
331− Right (Session (DisconnectedTransaction _ context)) -> pure $ evalState (RE.evalRelationalExpr expr) context
345+ Right (Session (DisconnectedTransaction _ context)) -> case evalState (RE.evalRelationalExpr expr) context of
346+ Left err -> pure (Left err)
347+ Right rel -> pure (force (Right rel)) -- this is necessary so that any undefined/error exceptions are spit out here
332348executeRelationalExpr sessionId conn@(RemoteProcessConnection _ _) relExpr = remoteCall conn (ExecuteRelationalExpr sessionId relExpr)
333349
334350-- | Execute a database context expression in the context of the session and connection. Database expressions modify the current session's disconnected transaction but cannot modify the transaction graph.
335351executeDatabaseContextExpr :: SessionId -> Connection -> DatabaseContextExpr -> IO (Maybe RelationalError)
336−executeDatabaseContextExpr sessionId (InProcessConnection _ _ sessions _ _) expr = atomically $ do
352+executeDatabaseContextExpr sessionId (InProcessConnection _ _ sessions _ _) expr = excMaybe $ atomically $ do
337353 eSession <- sessionForSessionId sessionId sessions
338354 case eSession of
339355 Left err -> pure $ Just err
340− Right session -> case runState (RE.evalContextExpr expr) (sessionContext session) of
341− (Just err,_) -> return $ Just err
342− (Nothing, context') -> do
343− let newDiscon = DisconnectedTransaction (sessionParentId session) context'
344− newSession = Session newDiscon
345− STMMap.insert newSession sessionId sessions
346− return Nothing
356+ Right session -> do
357+ case runState (RE.evalContextExpr expr) (sessionContext session) of
358+ (Just err,_) -> return $ Just err
359+ (Nothing, context') -> do
360+ let newDiscon = DisconnectedTransaction (sessionParentId session) context'
361+ newSession = Session newDiscon
362+ STMMap.insert newSession sessionId sessions
363+ pure Nothing
364+
347365executeDatabaseContextExpr sessionId conn@(RemoteProcessConnection _ _) dbExpr = remoteCall conn (ExecuteDatabaseContextExpr sessionId dbExpr)
348366
349367-- | Execute a database context IO-monad-based expression for the given session and connection. `DatabaseContextIOExpr` modify the DatabaseContext but cannot be purely implemented.
350368--this is almost completely identical to executeDatabaseContextExpr above
351369executeDatabaseContextIOExpr :: SessionId -> Connection -> DatabaseContextIOExpr -> IO (Maybe RelationalError)
352−executeDatabaseContextIOExpr sessionId (InProcessConnection _ _ sessions _ scriptSession) expr = do
370+executeDatabaseContextIOExpr sessionId (InProcessConnection _ _ sessions _ scriptSession) expr = excMaybe $ do
353371 eSession <- atomically $ sessionForSessionId sessionId sessions --potentially race condition due to interleaved IO?
354372 case eSession of
355373 Left err -> pure $ Just err
@@ -390,7 +408,7 @@ executeCommitExprSTM_ oldContext newContext nodes = do
390408
391409-- | Execute a transaction graph expression in the context of the session and connection. Transaction graph operators modify the transaction graph state.
392410executeGraphExpr :: SessionId -> Connection -> TransactionGraphOperator -> IO (Maybe RelationalError)
393−executeGraphExpr sessionId (InProcessConnection strat clientNodes sessions graphTvar _) graphExpr = do
411+executeGraphExpr sessionId (InProcessConnection strat clientNodes sessions graphTvar _) graphExpr = excMaybe $ do
394412 freshId <- nextRandom
395413 manip <- atomically $ do
396414 eSession <- sessionForSessionId sessionId sessions
src/lib/ProjectM36/Error.hs+1
@@ -69,6 +69,7 @@ data RelationalError = NoSuchAttributeNamesError (S.Set AttributeName)
6969 | NotificationNameNotInUseError NotificationName
7070 | ImportError T.Text -- really? This should be broken out into some other error type- this has nothing to do with relational algebra
7171 | ExportError T.Text
72+ | UnhandledExceptionError String
7273 | MergeTransactionError MergeError
7374 | AtomFunctionBodyScriptError AtomFunctionBodyCompilationError
7475 | MultipleErrors [RelationalError]
test/TutorialD/Interpreter/AtomFunctionScript.hs+31−4
@@ -4,23 +4,50 @@ import Test.HUnit
44import ProjectM36.Client
55import ProjectM36.Relation
66import ProjectM36.Base
7+import ProjectM36.Error
78import qualified Data.Vector as V
89import qualified Data.Map as M
910
1011main :: IO ()
1112main = do
12− tcounts <- runTestTT (TestList [testBasicAtomFunction])
13+ tcounts <- runTestTT (TestList [testBasicAtomFunction,
14+ testErrorAtomFunction])
1315 if errors tcounts + failures tcounts > 0 then exitFailure else exitSuccess
1416
17+--add an atom function and run it
1518testBasicAtomFunction :: Test
1619testBasicAtomFunction = TestCase $ do
17− --add an atom function and run it
1820 (sess, conn) <- dateExamplesConnection emptyNotificationCallback
19− executeTutorialD sess conn "addatomfunction \"mkTest\" Int -> Int \"\\(IntAtom x:xs) -> IntAtom x\""
21+ executeTutorialD sess conn "addatomfunction \"mkTest\" Int -> Int \"\\\\(IntAtom x:xs) -> IntAtom x\""
2022 let attrs = [Attribute "x" IntAtomType]
2123 funcAtomExpr = FunctionAtomExpr "mkTest" [NakedAtomExpr (IntAtom 3)]
2224 tupleExprs = [TupleExpr (M.singleton "x" funcAtomExpr)]
2325 expectedResult = mkRelationFromList (V.fromList attrs) [[IntAtom 3]]
2426 result <- executeRelationalExpr sess conn (MakeRelationFromExprs (Just (map NakedAttributeExpr attrs)) tupleExprs)
2527
26− assertEqual "simple atom function equality" expectedResult result
28+ assertEqual "simple atom function equality" expectedResult result
29+
30+--add an atom function which bombs out
31+testErrorAtomFunction :: Test
32+testErrorAtomFunction = TestCase $ do
33+ (sess, conn) <- dateExamplesConnection emptyNotificationCallback
34+ executeTutorialD sess conn "addatomfunction \"mkTest\" Int -> Int \"\"\"\\(IntAtom x:xs) -> (error (show 1) ) :: Atom\"\"\""
35+ let attrs = [Attribute "x" IntAtomType]
36+ funcAtomExpr = FunctionAtomExpr "mkTest" [NakedAtomExpr (IntAtom 3)]
37+ tupleExprs = [TupleExpr (M.singleton "x" funcAtomExpr)]
38+ expectedResult = Left (UnhandledExceptionError "1")
39+ result <- executeRelationalExpr sess conn (MakeRelationFromExprs (Just (map NakedAttributeExpr attrs)) tupleExprs)
40+ assertEqual "catch error exception from script" expectedResult result
41+
42+testNoArgumentAtomFunction :: Test
43+testNoArgumentAtomFunction = TestCase $ do
44+ (sess, conn) <- dateExamplesConnection emptyNotificationCallback
45+ executeTutorialD sess conn "addatomfunction \"mkTest\" Int -> Int \"\"\"\\(x :: [Atom]) -> IntAtom 5\"\"\""
46+ let attrs = [Attribute "x" IntAtomType]
47+ funcAtomExpr = FunctionAtomExpr "mkTest" []
48+ tupleExprs = [TupleExpr (M.singleton "x" funcAtomExpr)]
49+ expectedResult = mkRelationFromList (V.fromList attrs) [[IntAtom 5]]
50+ result <- executeRelationalExpr sess conn (MakeRelationFromExprs (Just (map NakedAttributeExpr attrs)) tupleExprs)
51+ assertEqual "no argument scripted function" expectedResult result
52+
53+