git.iko.soy/distributed-process
on master help sign in
Merge branch 'semi-typed-recv'

Merge semi-typed-recv into master This changeset provides deferred matching for AbstractMessage. The 'maybeHandleMessage' capability enables a common pattern, where one layer of code is managing the process' mailbox and knows nothing about which types can be handled, whilst another layer does not access the mailbox, but is able to handle specific message types. Building on this 'deferred type checking' we provide an exception handler that can process a range of different exit reasons.

commite7cd226117aa0227cd44e4b9a96e277966bf99a4 authorTim Watson <tim@rabbitmq.com> dateFri 18 Jan 2013, 17:41 (14 years ago) parentc88c5c0e2cd2d5
4 files changed+248−20
distributed-process/ChangeLog+4 distributed-process/src/Control/Distributed/Process.hs+10−2 distributed-process/src/Control/Distributed/Process/Internal/Primitives.hs+99−18 distributed-process/tests/TestCH.hs+135
distributed-process/ChangeLog+4
@@ -1,3 +1,7 @@
1+
2+* Expose deferred type checking for AbstractMessage
3+* Provide improved exception handling for deferred type checked exit reasons
4+
152012-11-22 Edsko de Vries <edsko@well-typed.com> 0.4.1
26
37* Make behaviour of 'register' more Erlang-like (register will now fail if the
distributed-process/src/Control/Distributed/Process.hs+10−2
@@ -5,7 +5,7 @@
55-- Peyton Jones
66-- (<http://research.microsoft.com/en-us/um/people/simonpj/papers/parallel/>),
77-- although some of the details are different. The precise message passing
8−-- semantics are based on /A unified semantics for future Erlang/ by Hans
8+-- semantics are based on /A unified semantics for future Erlang/ by Hans
99-- Svensson, Lars-Åke Fredlund and Clara Benac Earle.
1010module Control.Distributed.Process
1111 ( -- * Basic types
@@ -39,6 +39,7 @@ module Control.Distributed.Process
3939 , matchUnknown
4040 , AbstractMessage(..)
4141 , matchAny
42+ , matchAnyIf
4243 , matchChan
4344 -- * Process management
4445 , spawn
@@ -48,6 +49,7 @@ module Control.Distributed.Process
4849 , kill
4950 , exit
5051 , catchExit
52+ , catchesExit
5153 , ProcessTerminationException(..)
5254 , ProcessRegistrationException(..)
5355 , SpawnRef
@@ -192,6 +194,7 @@ import Control.Distributed.Process.Internal.Primitives
192194 , matchUnknown
193195 , AbstractMessage(..)
194196 , matchAny
197+ , matchAnyIf
195198 , matchChan
196199 -- Process management
197200 , terminate
@@ -199,6 +202,7 @@ import Control.Distributed.Process.Internal.Primitives
199202 , die
200203 , exit
201204 , catchExit
205+ , catchesExit
202206 , kill
203207 , getSelfPid
204208 , getSelfNode
@@ -361,7 +365,11 @@ spawnMonitor nid proc = do
361365-- "Control.Distributed.Process.Closure".
362366--
363367-- See also 'spawn'.
364−call :: Serializable a => Static (SerializableDict a) -> NodeId -> Closure (Process a) -> Process a
368+call :: Serializable a
369+ => Static (SerializableDict a)
370+ -> NodeId
371+ -> Closure (Process a)
372+ -> Process a
365373call dict nid proc = do
366374 us <- getSelfPid
367375 (pid, mRef) <- spawnMonitor nid (proc `bindCP` cpSend dict us)
distributed-process/src/Control/Distributed/Process/Internal/Primitives.hs+99−18
@@ -21,6 +21,7 @@ module Control.Distributed.Process.Internal.Primitives
2121 , matchUnknown
2222 , AbstractMessage(..)
2323 , matchAny
24+ , matchAnyIf
2425 , matchChan
2526 -- * Process management
2627 , terminate
@@ -29,6 +30,9 @@ module Control.Distributed.Process.Internal.Primitives
2930 , kill
3031 , exit
3132 , catchExit
33+ , catchesExit
34+ -- keep the exception constructor hidden, so that handling exit
35+ -- reasons must take place via the 'catchExit' family of primitives
3236 , ProcessExitException()
3337 , getSelfPid
3438 , getSelfNode
@@ -311,24 +315,68 @@ matchIf c p = Match $ MatchMsg $ \msg ->
311315 !decoded = decode (messageEncoding msg)
312316 _ -> Nothing
313317
318+-- | Represents a received message and provides two basic operations on it.
314319data AbstractMessage = AbstractMessage {
315− forward :: ProcessId -> Process ()
320+ forward :: ProcessId -> Process () -- ^ forward the message to @ProcessId@
321+ , maybeHandleMessage :: forall a b. (Serializable a)
322+ => (a -> Process b) -> Process (Maybe b) {- ^ Handle the message.
323+ If the type of the message matches the type of the first argument to
324+ the supplied expression, then the expression will be evaluated against
325+ it. If this runtime type checking fails, then @Nothing@ will be returned
326+ to indicate the fact. If the check succeeds and evaluation proceeds
327+ however, the resulting value with be wrapped with @Just@.
328+ -}
316329 }
317330
318−-- | Match against an arbitrary message
331+-- | Match against an arbitrary message. 'matchAny' removes the first available
332+-- message from the process mailbox, and via the 'AbstractMessage' type,
333+-- supports forwarding /or/ handling the message /if/ it is of the correct
334+-- type. If /not/ of the right type, then the 'AbstractMessage'
335+-- @maybeHandleMessage@ function will not evaluate the supplied expression,
336+-- /but/ the message will still have been removed from the process mailbox!
337+--
319338matchAny :: forall b. (AbstractMessage -> Process b) -> Match b
320−matchAny p = Match $ MatchMsg $ Just . p . abstract
321− where
322− abstract :: Message -> AbstractMessage
323− abstract msg = AbstractMessage {
324− forward = \them -> do
325− proc <- ask
326− liftIO $ sendPayload (processNode proc)
327− (ProcessIdentifier (processId proc))
328− (ProcessIdentifier them)
329− NoImplicitReconnect
330− (messageToPayload msg)
331− }
339+matchAny p = Match $ MatchMsg $ Just . p . abstract
340+
341+-- | Match against an arbitrary message. 'matchAnyIf' will /only/ remove the
342+-- message from the process mailbox, /if/ the supplied condition matches. The
343+-- success (or failure) of runtime type checks in @maybeHandleMessage@ does not
344+-- count here, i.e., if the condition evaluates to @True@ then the message will
345+-- be removed from the process mailbox and decoded, but that does /not/
346+-- guarantee that an expression passed to @maybeHandleMessage@ will pass the
347+-- runtime type checks and therefore be evaluated. If the types do not match
348+-- up, then @maybeHandleMessage@ returns 'Nothing'.
349+matchAnyIf :: forall a b. (Serializable a)
350+ => (a -> Bool)
351+ -> (AbstractMessage -> Process b)
352+ -> Match b
353+matchAnyIf c p = Match $ MatchMsg $ \msg ->
354+ case messageFingerprint msg == fingerprint (undefined :: a) of
355+ True | c decoded -> Just (p (abstract msg))
356+ where
357+ decoded :: a
358+ -- Make sure the value is fully decoded so that we don't hang to
359+ -- bytestrings when the calling process doesn't evaluate immediately
360+ !decoded = decode (messageEncoding msg)
361+ _ -> Nothing
362+
363+abstract :: Message -> AbstractMessage
364+abstract msg = AbstractMessage {
365+ forward = \them -> do
366+ proc <- ask
367+ liftIO $ sendPayload (processNode proc)
368+ (ProcessIdentifier (processId proc))
369+ (ProcessIdentifier them)
370+ NoImplicitReconnect
371+ (messageToPayload msg)
372+ , maybeHandleMessage = \(proc :: (a -> Process b)) -> do
373+ case messageFingerprint msg == fingerprint (undefined :: a) of
374+ True -> do { r <- proc (decoded :: a); return (Just r) }
375+ where
376+ decoded :: a
377+ !decoded = decode (messageEncoding msg)
378+ _ -> return Nothing
379+ }
332380
333381-- | Remove any message from the queue
334382matchUnknown :: Process b -> Match b
@@ -349,13 +397,15 @@ terminate :: Process a
349397terminate = liftIO $ throwIO ProcessTerminationException
350398
351399-- [Issue #110]
352−-- | Die immediately - throws a 'ProcessExitException' with the given @reason@
400+-- | Die immediately - throws a 'ProcessExitException' with the given @reason@.
353401die :: Serializable a => a -> Process b
354402die reason = do
355403 pid <- getSelfPid
356404 liftIO $ throwIO (ProcessExitException pid (createMessage reason))
357405
358−-- | Forceful request to kill a process
406+-- | Forceful request to kill a process. Where 'exit' provides an exception
407+-- that can be caught and handled, 'kill' throws an unexposed exception type
408+-- which cannot be handled explicitly (by type).
359409kill :: ProcessId -> String -> Process ()
360410-- NOTE: We send the message to our local node controller, which will then
361411-- forward it to a remote node controller (if applicable). Sending it directly
@@ -363,7 +413,9 @@ kill :: ProcessId -> String -> Process ()
363413-- 'monitor' or 'link' request.
364414kill them reason = sendCtrlMsg Nothing (Kill them reason)
365415
366−-- | Graceful request to exit a process
416+-- | Graceful request to exit a process. Throws 'ProcessExitException' with the
417+-- supplied @reason@ encoded as a message. Any /exit signal/ raised in this
418+-- manner can be handled using the 'catchExit' family of functions.
367419exit :: Serializable a => ProcessId -> a -> Process ()
368420-- NOTE: We send the message to our local node controller, which will then
369421-- forward it to a remote node controller (if applicable). Sending it directly
@@ -371,7 +423,13 @@ exit :: Serializable a => ProcessId -> a -> Process ()
371423-- 'monitor' or 'link' request.
372424exit them reason = sendCtrlMsg Nothing (Exit them (createMessage reason))
373425
374−-- | Catches ProcessExitException
426+-- | Catches 'ProcessExitException'. The handler will not be applied unless its
427+-- type matches the encoded data stored in the exception (see the /reason/
428+-- argument given to the 'exit' primitive). If the handler cannot be applied,
429+-- the exception will be re-thrown.
430+--
431+-- To handle 'ProcessExitException' without regard for /reason/, see 'catch'.
432+-- To handle multiple /reasons/ of differing types, see 'catchesExit'.
375433catchExit :: forall a b . (Show a, Serializable a)
376434 => Process b
377435 -> (ProcessId -> a -> Process b)
@@ -388,6 +446,29 @@ catchExit act exitHandler = catch act handleExit
388446 -- bytestrings if the caller doesn't use the value immediately
389447 !decoded = decode (messageEncoding msg)
390448
449+-- | As 'Control.Exception.catches' but allows for multiple handlers. Because
450+-- 'ProcessExitException' stores the exit @reason@ as a typed, encoded message,
451+-- a handler must accept an input of the expected type. In order to handle
452+-- a list of potentially different handlers (and therefore input types), a
453+-- handler passed to 'catchesExit' must accept 'AbstractMessage' and return
454+-- @Maybe@ (i.e., @Just p@ if it handled the exit reason, otherwise @Nothing@).
455+--
456+-- See 'maybeHandleMessage' and 'AsbtractMessage' for more details.
457+catchesExit :: Process b
458+ -> [(ProcessId -> AbstractMessage -> (Process (Maybe b)))]
459+ -> Process b
460+catchesExit act handlers = catch act ((flip handleExit) handlers)
461+ where
462+ handleExit :: ProcessExitException
463+ -> [(ProcessId -> AbstractMessage -> Process (Maybe b))]
464+ -> Process b
465+ handleExit ex [] = liftIO $ throwIO ex
466+ handleExit ex@(ProcessExitException from msg) (h:hs) = do
467+ r <- h from (abstract msg)
468+ case r of
469+ Nothing -> handleExit ex hs
470+ Just p -> return p
471+
391472-- | Our own process ID
392473getSelfPid :: Process ProcessId
393474getSelfPid = processId <$> ask
distributed-process/tests/TestCH.hs+135
@@ -757,6 +757,121 @@ testMatchAny transport = do
757757
758758 takeMVar clientDone
759759
760+-- | Test 'matchAny'. This repeats the 'testMath' but with a proxy server
761+-- in between, however we block 'Divide' requests ....
762+testMatchAnyHandle :: NT.Transport -> Assertion
763+testMatchAnyHandle transport = do
764+ proxyAddr <- newEmptyMVar
765+ clientDone <- newEmptyMVar
766+
767+ -- Math server
768+ forkIO $ do
769+ localNode <- newLocalNode transport initRemoteTable
770+ mathServer <- forkProcess localNode math
771+ proxyServer <- forkProcess localNode $ forever $ do
772+ receiveWait [
773+ matchAny (maybeForward mathServer)
774+ ]
775+ putMVar proxyAddr proxyServer
776+
777+ -- Client
778+ forkIO $ do
779+ localNode <- newLocalNode transport initRemoteTable
780+ mathServer <- readMVar proxyAddr
781+
782+ runProcess localNode $ do
783+ pid <- getSelfPid
784+ send mathServer (Add pid 1 2)
785+ 3 <- expect :: Process Double
786+ send mathServer (Divide pid 8 2)
787+ Nothing <- (expectTimeout 100000) :: Process (Maybe Double)
788+ liftIO $ putMVar clientDone ()
789+
790+ takeMVar clientDone
791+ where maybeForward :: ProcessId -> AbstractMessage -> Process (Maybe ())
792+ maybeForward s msg =
793+ maybeHandleMessage msg (\m@(Add _ _ _) -> send s m)
794+
795+testMatchAnyNoHandle :: NT.Transport -> Assertion
796+testMatchAnyNoHandle transport = do
797+ addr <- newEmptyMVar
798+ clientDone <- newEmptyMVar
799+ serverDone <- newEmptyMVar
800+
801+ -- Math server
802+ forkIO $ do
803+ localNode <- newLocalNode transport initRemoteTable
804+ server <- forkProcess localNode $ forever $ do
805+ receiveWait [
806+ matchAnyIf
807+ -- the condition has type `Add -> Bool`
808+ (\(Add _ _ _) -> True)
809+ -- the match `AbstractMessage -> Process ()` will succeed!
810+ (\m -> do
811+ -- `String -> Process ()` does *not* match the input types however
812+ r <- (maybeHandleMessage m (\(_ :: String) -> die "NONSENSE" ))
813+ case r of
814+ Nothing -> return ()
815+ Just _ -> die "NONSENSE")
816+ ]
817+ -- we *must* have removed the message from our mailbox though!!!
818+ Nothing <- receiveTimeout 100000 [ match (\(Add _ _ _) -> return ()) ]
819+ liftIO $ putMVar serverDone ()
820+ putMVar addr server
821+
822+ -- Client
823+ forkIO $ do
824+ localNode <- newLocalNode transport initRemoteTable
825+ server <- readMVar addr
826+
827+ runProcess localNode $ do
828+ pid <- getSelfPid
829+ send server (Add pid 1 2)
830+ -- we only care about the client having sent a message, so we're done
831+ liftIO $ putMVar clientDone ()
832+
833+ takeMVar clientDone
834+ takeMVar serverDone
835+
836+-- | Test 'matchAnyIf'. We provide an /echo/ server, but it ignores requests
837+-- unless the text body @/= "bar"@ - this case should time out rather than
838+-- removing the message from the process mailbox.
839+testMatchAnyIf :: NT.Transport -> Assertion
840+testMatchAnyIf transport = do
841+ echoAddr <- newEmptyMVar
842+ clientDone <- newEmptyMVar
843+
844+ -- echo server
845+ forkIO $ do
846+ localNode <- newLocalNode transport initRemoteTable
847+ echoServer <- forkProcess localNode $ forever $ do
848+ receiveWait [
849+ matchAnyIf (\(_ :: ProcessId, (s :: String)) -> s /= "bar")
850+ handleMessage
851+ ]
852+ putMVar echoAddr echoServer
853+
854+ -- Client
855+ forkIO $ do
856+ localNode <- newLocalNode transport initRemoteTable
857+ server <- readMVar echoAddr
858+
859+ runProcess localNode $ do
860+ pid <- getSelfPid
861+ send server (pid, "foo")
862+ "foo" <- expect
863+ send server (pid, "baz")
864+ "baz" <- expect
865+ send server (pid, "bar")
866+ Nothing <- (expectTimeout 100000) :: Process (Maybe Double)
867+ liftIO $ putMVar clientDone ()
868+
869+ takeMVar clientDone
870+ where handleMessage :: AbstractMessage -> Process (Maybe ())
871+ handleMessage msg =
872+ maybeHandleMessage msg (\(pid :: ProcessId, (m :: String))
873+ -> do { send pid m; return () })
874+
760875-- Test 'receiveChanTimeout'
761876testReceiveChanTimeout :: NT.Transport -> Assertion
762877testReceiveChanTimeout transport = do
@@ -894,6 +1009,22 @@ testKillRemote transport = do
8941009
8951010 takeMVar done
8961011
1012+testCatchesExit :: NT.Transport -> Assertion
1013+testCatchesExit transport = do
1014+ localNode <- newLocalNode transport initRemoteTable
1015+ done <- newEmptyMVar
1016+
1017+ _ <- forkProcess localNode $ do
1018+ (die ("foobar", 123 :: Int))
1019+ `catchesExit` [
1020+ (\_ m -> maybeHandleMessage m (\(_ :: String) -> return ()))
1021+ , (\_ m -> maybeHandleMessage m (\(_ :: Maybe Int) -> return ()))
1022+ , (\_ m -> maybeHandleMessage m (\(_ :: String, _ :: Int)
1023+ -> (liftIO $ putMVar done ()) >> return ()))
1024+ ]
1025+
1026+ takeMVar done
1027+
8971028testDie :: NT.Transport -> Assertion
8981029testDie transport = do
8991030 localNode <- newLocalNode transport initRemoteTable
@@ -987,12 +1118,16 @@ tests (transport, transportInternals) = [
9871118 , testCase "RemoteRegistry" (testRemoteRegistry transport)
9881119 , testCase "SpawnLocal" (testSpawnLocal transport)
9891120 , testCase "MatchAny" (testMatchAny transport)
1121+ , testCase "MatchAnyHandle" (testMatchAnyHandle transport)
1122+ , testCase "MatchAnyNoHandle" (testMatchAnyNoHandle transport)
1123+ , testCase "MatchAnyIf" (testMatchAnyIf transport)
9901124 , testCase "ReceiveChanTimeout" (testReceiveChanTimeout transport)
9911125 , testCase "ReceiveChanFeatures" (testReceiveChanFeatures transport)
9921126 , testCase "KillLocal" (testKillLocal transport)
9931127 , testCase "KillRemote" (testKillRemote transport)
9941128 , testCase "Die" (testDie transport)
9951129 , testCase "PrettyExit" (testPrettyExit transport)
1130+ , testCase "CatchesExit" (testCatchesExit transport)
9961131 , testCase "ExitLocal" (testExitLocal transport)
9971132 , testCase "ExitRemote" (testExitRemote transport)
9981133 ]