Skip to content

Commit 34dcf39

Browse files
committed
more documentation/tutorial improvements
1 parent e3b7172 commit 34dcf39

File tree

4 files changed

+89
-48
lines changed

4 files changed

+89
-48
lines changed

Diff for: documentation.md

+13-15
Original file line numberDiff line numberDiff line change
@@ -238,14 +238,14 @@ types such as `TMVar` just as normal Haskell threads would.
238238
### Typed Channels
239239

240240
Channels provides an alternative to message transmission with `send` and `expect`.
241-
While `send` and `expect` allow transmission of messages of any `Serializable`
241+
While `send` and `expect` allow us to transmit messages of any `Serializable`
242242
type, channels require a uniform type. Channels work like a distributed equivalent
243243
of Haskell's `Control.Concurrent.Chan`, however they have distinct ends: a single
244244
receiving port and a corollary send port.
245245

246246
Channels provide a nice alternative to *bare send and receive*, which is a bit
247-
*unHaskellish*, because the processes message queue has messages of multiple
248-
types, and we have to do dynamic type checking.
247+
*un-Haskell-ish*, since our process' message queue can contain messages of multiple
248+
types, forcing us to undertake dynamic type checking at runtime.
249249

250250
We create channels with a call to `newChan`, and send/receive on them using the
251251
`{send,receive}Chan` primitives:
@@ -264,19 +264,17 @@ channelsDemo = do
264264
{% endhighlight %}
265265

266266
Channels are particularly useful when you are sending a message that needs a
267-
response, because the code that receives the response knows exactly where it
268-
came from - i.e., it knows that it came from the `SendPort` connected to
269-
the `ReceivePort` on which it just received a response.
267+
response, because we know exactly where to look for the reply.
270268

271-
Channels can sometimes allows message types to be simplified, as passing a
272-
`ProcessId` to reply to isn't required. Channels are not so useful when you
273-
need to spawn a process and then send a bunch a messages to it and wait for
274-
replies, because we can’t send the `ReceivePort`.
269+
Channels can also allow message types to be simplified, as passing a
270+
`ProcessId` for the reply isn't required. Channels aren't so useful when we
271+
need to spawn a process and send a bunch a messages to it, then wait for
272+
replies however; we can’t send a `ReceivePort` since it is not `Serializable`.
275273

276-
ReceivePorts can be merged, so you can listen on several simultaneously. In the
277-
latest version of [distributed-process][2], you can listen for *regular* messages
278-
and on multiple channels at the same time, using `matchChan` in the list of
279-
allowed matches passed `receive`.
274+
`ReceivePort`s can be merged, so we can listen on several simultaneously. In the
275+
latest version of [distributed-process][2], we can listen for *regular* messages
276+
and multiple channels at the same time, using `matchChan` in the list of
277+
allowed matches passed `receiveWait` and `receiveTimeout`.
280278

281279
### Linking and monitoring
282280

@@ -318,7 +316,7 @@ function, which sends an exit signal that cannot be handled.
318316
#### __An important note about exit signals__
319317

320318
Exit signals in Cloud Haskell are unlike asynchronous exceptions in regular
321-
haskell code. Whilst processes *can* use asynchronous exceptions - there's
319+
haskell code. Whilst a process *can* use asynchronous exceptions - there's
322320
nothing stoping this since the `Process` monad is an instance of `MonadIO` -
323321
exceptions thrown are not bound by the same ordering guarantees as messages
324322
delivered to a process. Link failures and exit signals *might* be implemented

Diff for: tutorials/ch-tutorial3.md

+74-31
Original file line numberDiff line numberDiff line change
@@ -8,34 +8,38 @@ title: Getting to know Processes
88
### The Thing About Nodes
99

1010
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_.
1516

1617
Architecturally, Cloud Haskell's _Node Controller_ consists of a pair of message
1718
buss processes, one of which listens for network-transport level events whilst the
1819
other is busy processing _signal events_ (most of which pertain to either message
1920
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.
2126

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
2328
more detail...
2429

2530
### Message Ordering
2631

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:
3034

3135
1. sending is asynchronous (i.e., it does not block the caller)
3236
2. sending _never_ fails, regardless of the state of the recipient process
3337
3. even if a message is received, there is **no** guarantee *when* it will arrive
3438
4. there are **no** guarantees that the message will be received at all
3539

3640
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.
3943
Consider a stream of messages sent from one process to another. If the
4044
stream consists of messages `a, b, c` and we have seen `c`, then we know for
4145
certain that we will have already seen `a, b` (in that order), so long as the
@@ -58,15 +62,14 @@ their mailbox.
5862

5963
Processes dequeue messages (from their mailbox) using the [`expect`][1]
6064
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
6266
is found.
6367

6468
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:
7073

7174
{% highlight haskell %}
7275
demo :: Process ()
@@ -79,23 +82,23 @@ demo = do
7982
listen = do
8083
third <- expect :: Process ProcessId
8184
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]
8487
send third ()
8588
{% endhighlight %}
8689

8790
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.
99102

100103
By contrast, the [`recieve`][2] family of primitives take a list of `Match`
101104
objects, each derived from evaluating a [`match`][3] style primitive. This
@@ -292,7 +295,47 @@ some `DiedReason` other than `DiedNormal`).
292295
Monitors on the other hand, do not cause the *listening* process to exit at all, instead
293296
putting a `ProcessMonitorNotification` into the process' mailbox. This signal and its
294297
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.
296339

297340
Linking and monitoring are foundational tools for *supervising* processes, where a top level
298341
process manages a set of children, starting, stopping and restarting them as necessary.

Diff for: tutorials/ch-tutorial4.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,7 @@ cheap, but not free as each process is a haskell thread, plus some additional bo
406406
keeping data.
407407

408408
The cost of spawning two processes for each computation/task might represent just that
409-
bit too much overhead for some applications. In our next tutorial, we'll look at the
409+
bit too much overhead for some applications. In a forthcoming tutorial, we'll look at the
410410
`Control.Distributed.Process.Platform.Task` API, which looks a lot like `Async` but
411411
manages exit signals in a single thread and makes configurable task pools and task
412412
supervision strategy part of its API.

Diff for: tutorials/ch-tutorial5.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ title: Supervision Principles
77

88
### Introduction
99

10-
In the previous tutorial, we looked at utilities for linking processes together
10+
In previous tutorial, we've looked at utilities for linking processes together
1111
and monitoring their lifecycle as it changes. The ability to link and monitor are
1212
foundational tools for building _reliable_ systems, and are the bedrock principles
1313
on which Cloud Haskell's supervision capabilities are built.

0 commit comments

Comments
 (0)