@@ -8,34 +8,38 @@ title: Getting to know Processes
8
8
### The Thing About Nodes
9
9
10
10
Before we can really get to know _ processes_ , we need to consider the role of
11
- the _ Node Controller_ in Cloud Haskell. As per the [ _ semantics_ ] [ 4 ] , Cloud
12
- Haskell makes the role of _ Node Controller_ (occasionally referred to by the original
13
- "Unified Semantics for Future Erlang" paper on which our semantics are modelled
14
- as the "ether") explicit.
11
+ the _ Node Controller_ in Cloud Haskell. In our formal [ _ semantics_ ] [ 4 ] , Cloud
12
+ Haskell hides the role of _ Node Controller_ (explicitly defined in the original
13
+ "Unified Semantics for Future Erlang" paper on which our semantics are modelled).
14
+ Nonetheless, each Cloud Haskell _ node_ is serviced and managed by a
15
+ conceptual _ Node Controller_ .
15
16
16
17
Architecturally, Cloud Haskell's _ Node Controller_ consists of a pair of message
17
18
buss processes, one of which listens for network-transport level events whilst the
18
19
other is busy processing _ signal events_ (most of which pertain to either message
19
20
delivery or process lifecycle notification). Both these _ event loops_ runs sequentially
20
- in the system at all times.
21
+ in the system at all times. Messages are delivered via the _ Node Controller's_
22
+ _ event loops_ , which broadly correspond to the _ system queue (or "ether")_ mentioned
23
+ in the [ _ semantics_ ] [ 4 ] . The _ system queue_ delivers messages to individual process
24
+ mailboxes in a completely transparent fashion, leaving us with the illusion that
25
+ processes exist in a unidimensional space.
21
26
22
- With this in mind, let's consider Cloud Haskell's lightweight processes in a bit
27
+ With all this in mind, let's consider Cloud Haskell's lightweight processes in a bit
23
28
more detail...
24
29
25
30
### Message Ordering
26
31
27
- We have already met the ` send ` primitive, which is used to deliver
28
- a message to another process. Here's a review of what we've learned
29
- about ` send ` thus far:
32
+ We have already met the ` send ` primitive, used to deliver messages from one
33
+ process to another. Here's a review of what we've learned about ` send ` thus far:
30
34
31
35
1 . sending is asynchronous (i.e., it does not block the caller)
32
36
2 . sending _ never_ fails, regardless of the state of the recipient process
33
37
3 . even if a message is received, there is ** no** guarantee * when* it will arrive
34
38
4 . there are ** no** guarantees that the message will be received at all
35
39
36
40
Asynchronous sending buys us several benefits. Improved concurrency is
37
- possible, because processes do not need to block and wait for acknowledgements
38
- and error handling need not be implemented each time a message is sent.
41
+ possible, because processes need not block or wait for acknowledgements,
42
+ nor does error handling need to be implemented each time a message is sent.
39
43
Consider a stream of messages sent from one process to another. If the
40
44
stream consists of messages ` a, b, c ` and we have seen ` c ` , then we know for
41
45
certain that we will have already seen ` a, b ` (in that order), so long as the
@@ -58,15 +62,14 @@ their mailbox.
58
62
59
63
Processes dequeue messages (from their mailbox) using the [ ` expect ` ] [ 1 ]
60
64
and [ ` recieve ` ] [ 2 ] family of primitives. Both take an optional timeout,
61
- which leads to the expression evaluating to ` Nothing ` if no matching input
65
+ allowing the expression to evaluate to ` Nothing ` if no matching input
62
66
is found.
63
67
64
68
The [ ` expect ` ] [ 1 ] primitive blocks until a message matching the expected type
65
- (of the expression) is found in the process' mailbox. If such a message can be
66
- found by scanning the mailbox, it is dequeued and given to the caller. If no
67
- message (matching the expected type) can be found, the caller (i.e., the
68
- calling thread) is blocked until a matching message is delivered to the mailbox.
69
- Let's take a look at this in action:
69
+ (of the expression) is found in the process' mailbox. If a match is found by
70
+ scanning the mailbox, it is dequeued and given to the caller, otherwise the
71
+ caller (i.e., the calling thread) is blocked until a message of the expected
72
+ type is delivered to the mailbox. Let's take a look at this in action:
70
73
71
74
{% highlight haskell %}
72
75
demo :: Process ()
@@ -79,23 +82,23 @@ demo = do
79
82
listen = do
80
83
third <- expect :: Process ProcessId
81
84
first <- expect :: Process String
82
- Nothing <- expectTimeout 100000 :: Process String
83
- say first
85
+ second <- expectTimeout 100000 :: Process String
86
+ mapM _ ( say . show) [ first, second, third ]
84
87
send third ()
85
88
{% endhighlight %}
86
89
87
90
This program will print ` "hello" ` , then ` Nothing ` and finally ` pid://... ` .
88
- The first ` expect ` - labelled "third" because of the order in which it is
89
- due to be received - ** will** succeed, since the parent process sends its
90
- ` ProcessId ` after the string "hello", yet the listener blocks until it can dequeue
91
- the ` ProcessId ` before "expecting" a string. The second ` expect ` (labelled "first")
92
- also succeeds, demonstrating that the listener has selectively removed messages
93
- from its mailbox based on their type rather than the order in which they arrived.
94
- The third ` expect ` will timeout and evaluate to ` Nothing ` , because only one string
95
- is ever sent to the listener and that has already been removed from the mailbox.
96
- The removal of messages from the process' mailbox based on type is what makes this
97
- program viable - without this "selective receiving", the program would block and
98
- never complete.
91
+ The first ` expect ` - labelled "third" because of the order in which we
92
+ know it will arrive in our mailbox - ** will** succeed, since the parent process
93
+ sends its ` ProcessId ` after the string "hello", yet the listener blocks until it
94
+ can dequeue the ` ProcessId ` before "expecting" a string. The second ` expect `
95
+ (labelled "first") also succeeds, demonstrating that the listener has selectively
96
+ removed messages from its mailbox based on their type rather than the order in
97
+ which they arrived. The third ` expect ` will timeout and evaluate to ` Nothing ` ,
98
+ because only one string is ever sent to the listener and that has already been
99
+ removed from the mailbox. The removal of messages from the process' mailbox based
100
+ on type is what makes this program viable - without this "selective receiving",
101
+ the program would block and never complete.
99
102
100
103
By contrast, the [ ` recieve ` ] [ 2 ] family of primitives take a list of ` Match `
101
104
objects, each derived from evaluating a [ ` match ` ] [ 3 ] style primitive. This
@@ -292,7 +295,47 @@ some `DiedReason` other than `DiedNormal`).
292
295
Monitors on the other hand, do not cause the * listening* process to exit at all, instead
293
296
putting a ` ProcessMonitorNotification ` into the process' mailbox. This signal and its
294
297
constituent fields can be introspected in order to decide what action (if any) the receiver
295
- can/should take in response to the monitored processes death.
298
+ can/should take in response to the monitored processes death. Let's take a look at how
299
+ monitors can be used to determine both when and _ how_ a process has terminated. Tucked
300
+ away in distributed-process-platform, the ` linkOnFailure ` primitive works just like our
301
+ built-in ` link ` except that it only terminates the process which evaluated it (the
302
+ _ linker_ ), if the process it is linking with (the _ linkee_ ) terminates abnormally.
303
+ Let's take a look...
304
+
305
+ {% highlight haskell %}
306
+ linkOnFailure them = do
307
+ us <- getSelfPid
308
+ tid <- liftIO $ myThreadId
309
+ void $ spawnLocal $ do
310
+ callerRef <- P.monitor us
311
+ calleeRef <- P.monitor them
312
+ reason <- receiveWait [
313
+ matchIf (\( ProcessMonitorNotification mRef _ _ ) ->
314
+ mRef == callerRef) -- nothing left to do
315
+ (\_ -> return DiedNormal)
316
+ , matchIf (\( ProcessMonitorNotification mRef' _ _ ) ->
317
+ mRef' == calleeRef)
318
+ (\( ProcessMonitorNotification _ _ r') -> return r')
319
+ ]
320
+ case reason of
321
+ DiedNormal -> return ()
322
+ _ -> liftIO $ throwTo tid (ProcessLinkException us reason)
323
+ {% endhighlight %}
324
+
325
+ As we can see, this code makes use of monitors to track both processes involved in the
326
+ link. In order to track _ both_ processes and react to changes in their status, it is
327
+ necessary to spawn a third process which will do the monitoring. This doesn't happen
328
+ with the built-in link primitive, but is necessary in this case since the link handling
329
+ code resides outside the _ Node Controller_ .
330
+
331
+ The two matches passed to ` receiveWait ` both handle a ` ProcessMonitorNotification ` , and
332
+ the predicate passed to ` matchIf ` is used to determine whether the notification we're
333
+ receiving is for the _ linker_ or the _ linkee_ . If the _ linker_ dies, we've nothing more
334
+ to do, since links are unidirectional. If the _ linkee_ dies however, we must examine
335
+ the ` DiedReason ` the ` ProcessMonitorNotification ` provides us with, to determine whether
336
+ the _ linkee_ exited normally (i.e., with ` DiedNormal ` ) or otherwise. In the latter case,
337
+ we throw a ` ProcessLinkException ` to the _ linker_ , which is exactly how an ordinary link
338
+ would behave.
296
339
297
340
Linking and monitoring are foundational tools for * supervising* processes, where a top level
298
341
process manages a set of children, starting, stopping and restarting them as necessary.
0 commit comments