Skip to content

Commit b4d984e

Browse files
committed
further tutorial improvements
1 parent 34dcf39 commit b4d984e

File tree

4 files changed

+416
-229
lines changed

4 files changed

+416
-229
lines changed

Diff for: documentation.md

+40-66
Original file line numberDiff line numberDiff line change
@@ -287,71 +287,27 @@ a set of children, starting, stopping and restarting them as necessary.
287287

288288
### Stopping Processes
289289

290-
Some processes, like the *outer* process in the previous example, will run until
291-
they've completed and then return their value. This is just as we find with IO action,
292-
and there is an instance of `MonadIO` for the `Process` monad, so you can `liftIO` if
293-
you need to evaluate IO actions.
294-
295290
Because processes are implemented with `forkIO` we might be tempted to stop
296291
them by throwing an asynchronous exception to the process, but this is almost
297-
certainly the wrong thing to do. Instead we might send a kind of poison pill,
298-
which the process *ought* to handle by shutting down gracefully. Unfortunately
299-
because of the asynchronous nature of sending, this is no good because `send`
300-
will not fail under any circumstances. In fact, because `send` doesn't block,
301-
we therefore have no way to know if the recipient existed at the time we sent the
302-
poison pill. Even if the recipient did exist, we still have no guarantee that
303-
the message we sent actually arrived - the network connection between the nodes
304-
could have broken, for example. Making this *shutdown* protocol synchronous is
305-
no good either - how long would we wait for a reply? Indefinitely?
306-
307-
Exit signals come in two flavours - those that can
308-
be caught and those that cannot. A call to
309-
`exit :: (Serializable a) => ProcessId -> a -> Process ()` will dispatch an
310-
exit signal to the specified process. These *signals* can be intercepted and
311-
handled by the destination process however, so if you need to terminate the
312-
process in a brutal way, you can use the `kill :: ProcessId -> String -> Process ()`
313-
function, which sends an exit signal that cannot be handled.
314-
315-
------
316-
#### __An important note about exit signals__
317-
318-
Exit signals in Cloud Haskell are unlike asynchronous exceptions in regular
319-
haskell code. Whilst a process *can* use asynchronous exceptions - there's
320-
nothing stoping this since the `Process` monad is an instance of `MonadIO` -
321-
exceptions thrown are not bound by the same ordering guarantees as messages
322-
delivered to a process. Link failures and exit signals *might* be implemented
323-
using asynchronous exceptions - that is the case in the current
324-
implementation - but these are implemented in such a fashion that if you
325-
send a message and *then* an exit signal, the message is guaranteed to arrive
326-
first.
327-
328-
You should avoid throwing your own exceptions in code where possible. Instead,
329-
you should terminate yourself, or another process, using the built-in primitives
330-
`exit`, `kill` and `die`.
331-
332-
{% highlight haskell %}
333-
exit pid reason -- force `pid` to exit - reason can be any `Serializable` message
334-
kill pid reason -- reason is a string - the *kill* signal cannot be caught
335-
die reason -- as 'exit' but kills *us*
336-
{% endhighlight %}
337-
338-
The `exit` and `kill` primitives do essentially the same thing, but catching
339-
the specific exception thrown by `kill` is impossible, making `kill` an
340-
*untrappable exit signal*. Of course you could trap **all** exceptions, but
341-
you already know that's a very bad idea right!?
342-
343-
The `exit` primitive is a little different. This provides support for trapping
344-
exit signals in a generic way, so long as your *exit handler* is able to
345-
recognise the underlying type of the 'exit reason'. This (reason for exiting)
346-
is stored as a raw `Message`, so if your handler takes the appropriate type
347-
as an input (and therefore the `Message` can be decoded and passed to the
348-
handler) then the handler will run. This is pretty much the same approach as
349-
exception handling using `Typeable`, except that we decide whether or not the
350-
exception can be handled based on the type of `reason` instead of the type of
351-
the exception itself.
352-
353-
Calling `die` will immediately raise an exit signal (i.e., `ProcessExitException`)
354-
in the calling process.
292+
certainly the wrong thing to do. Firstly, processes might reside on a remote
293+
node, in which case throwing an exception is impossible. Secondly, if we send
294+
some messages to a process' mailbox and then dispatch an exception to kill it,
295+
there is no guarantee that the subject will receive our message before being
296+
terminated by the asynchronous exception.
297+
298+
To terminate a process unconditionally, we use the `kill` primitive, which
299+
dispatches an asynchronous exception (killing the subject) safely, respecting
300+
remote calls to processes on disparate nodes and observing message ordering
301+
guarantees such that `send pid "hello" >> kill pid "goodbye"` behaves quite
302+
unsurprisingly, delivering the message before the kill signal.
303+
304+
Exit signals come in two flavours however - those that can be caught and those
305+
that cannot. Whilst a call to `kill` results in an _un-trappable_ exception,
306+
a call to `exit :: (Serializable a) => ProcessId -> a -> Process ()` will dispatch
307+
an exit signal to the specified process that can be caught. These *signals* are
308+
intercepted and handled by the destination process using `catchExit`, allowing
309+
the receiver to match on the `Serializable` datum tucked away in the *exit signal*
310+
and decide whether to oblige or not.
355311

356312
----
357313

@@ -463,6 +419,12 @@ The API for `Async` is fairly rich, so reading the haddocks is suggested.
463419

464420
#### Managed Processes
465421

422+
The main idea behind a `ManagedProcess` is to separate the functional
423+
and non-functional aspects of an actor. By functional, we mean whatever
424+
application specific task the actor performs, and by non-functional
425+
we mean the *concurrency* or, more precisely, handling of the process'
426+
mailbox and its interaction with other actors (i.e., clients).
427+
466428
Looking at *typed channels*, we noted that their insistence on a specific input
467429
domain was more *haskell-ish* than working with bare send and receive primitives.
468430
The `Async` sub-package also provides a type safe interface for receiving data,
@@ -471,12 +433,12 @@ although it is limited to running a computation and waiting for its result.
471433
The [Control.Distributed.Processes.Platform.ManagedProcess][21] API provides a
472434
number of different abstractions that can be used to achieve similar benefits
473435
in your code. It works by introducing a standard protocol between your process
474-
and the *world around*, which governs how to handle request/reply processing,
475-
exit signals, timeouts, sleep/hibernation with `threadDelay` and even provides
436+
and the *world outside*, which governs how to handle request/reply processing,
437+
exit signals, timeouts, sleeping/hibernation with `threadDelay` and even provides
476438
hooks that terminating processes can use to clean up residual state.
477439

478440
The [API documentation][21] is quite extensive, so here we will simply point
479-
out the obvious differences. A implemented implemented with `ManagedProcess`
441+
out the obvious differences. A process implemented with `ManagedProcess`
480442
can present a type safe API to its callers (and the server side code too!),
481443
although that's not its primary benefit. For a very simplified example:
482444

@@ -514,6 +476,18 @@ just provides callback functions which take some state and either return a
514476
new state and a reply, or just a new state. The process is *managed* in the
515477
sense that its mailbox is under someone else's control.
516478

479+
A NOTE ABOUT THE CALL API AND THAT IT WILL FAIL (WITH UNHANDLED MESSAGE) IF
480+
THE CALLER IS EXPECTING A TYPE THAT DIFFERS FROM THE ONE THE SERVER PLANS
481+
TO RETURN, SINCE THE RETURN TYPE IS ENCODED IN THE CALL-MESSAGE TYPE ITSELF.
482+
483+
TODO: WRITE A TEST TO PROVE THE ABOVE
484+
485+
TODO: ADD AN API BASED ON SESSION TYPES AS A KIND OF MANAGED PROCESS.....
486+
487+
In a forthcoming tutorial, we'll look at the `Control.Distributed.Process.Platform.Task`
488+
API, which looks a lot like `Async` but manages exit signals in a single thread and makes
489+
configurable task pools and task supervision strategy part of its API.
490+
517491
More complex examples of the `ManagedProcess` API can be seen in the
518492
[Managed Processes tutorial][22]. API documentation for HEAD is available
519493
[here][21].

Diff for: tutorials/ch-tutorial1.md

+15-11
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
---
22
layout: tutorial
33
categories: tutorial
4-
sections: ['Getting Started', 'Create a node', 'Sending messages', 'Spawning Remote Processes']
4+
sections: ['Getting Started', 'Installing from source', 'Creating a node', 'Sending messages', 'Spawning Remote Processes']
55
title: Getting Started
66
---
77

@@ -27,7 +27,7 @@ run `make` to obtain the complete set of source repositories for building
2727
Cloud Haskell. The additional makefiles bundled with the umbrella assume
2828
that you have a recent version of cabal-dev installed.
2929

30-
### Create a node
30+
### Creating a node
3131

3232
Cloud Haskell's *lightweight processes* reside on a "node", which must
3333
be initialised with a network transport implementation and a remote table.
@@ -78,16 +78,16 @@ will send one to ourselves!
7878
{% endhighlight %}
7979

8080
Lightweight processes are implemented as `forkIO` threads. In general we will
81-
try to forget about this implementation detail, but let us note that we
81+
try to forget about this implementation detail, but let's note that we
8282
haven't deadlocked our own thread by sending to and receiving from its mailbox
8383
in this fashion. Sending messages is a completely asynchronous operation - even
8484
if the recipient doesn't exist, no error will be raised and evaluating `send`
85-
will not block the caller, not even if the caller is sending messages to itself!
85+
will not block the caller, even if the caller is sending messages to itself!
8686

87-
Receiving works quite the other way around, blocking the caller until a message
87+
Receiving works the opposite way, blocking the caller until a message
8888
matching the expected type arrives in our (conceptual) mailbox. If multiple
89-
messages of that type are in the queue, they will be returned in FIFO
90-
order, otherwise the caller will be blocked until a message arrives that can be
89+
messages of that type are present in the mailbox, they're be returned in FIFO
90+
order, if not, the caller is blocked until a message arrives that can be
9191
decoded to the correct type.
9292

9393
Let's spawn two processes on the same node and have them talk to each other.
@@ -150,15 +150,14 @@ In the _echo server_ above, our first match prints out whatever string it
150150
receives. If first message in out mailbox is not a `String`, then our second
151151
match is evaluated. This, given a tuple `t :: (ProcessId, String)`, will send
152152
the `String` component back to the sender's `ProcessId`. If neither match
153-
succeeds, the echo server process blocks until another message arrives and
153+
succeeds, the echo server blocks until another message arrives and
154154
tries again.
155155

156156
### Serializable Data
157157

158158
Processes may send any datum whose type implements the `Serializable` typeclass,
159-
which is done indirectly by implementing `Binary` and deriving `Typeable`.
160-
Implementations are already provided for off of Cloud Haskell's primitives
161-
and the most commonly used data structures.
159+
which is done indirectly by deriving `Binary` and `Typeable`. Implementations are
160+
provided for most of Cloud Haskell's primitives and various common data types.
162161

163162
### Spawning Remote Processes
164163

@@ -212,6 +211,11 @@ main = do
212211
-- etc
213212
{% endhighlight %}
214213

214+
Note that we're not limited to sending `Closure`s - it is possible to send data
215+
without having static values, and assuming the receiving code is able to decode
216+
this data and operate on it, we can easily put together a simple AST that maps
217+
to operations we wish to execute remotely.
218+
215219
------
216220

217221
[1]: /static/doc/distributed-process/Control-Distributed-Process.html#v:Message

0 commit comments

Comments
 (0)