From 7b7c2a49b92702ea2553eeb27c2ab4a38931db63 Mon Sep 17 00:00:00 2001 From: Brian Anderson Date: Mon, 1 Oct 2012 18:42:11 -0700 Subject: [PATCH] docs: Begin integrating bblum's linked failure blog post --- doc/tutorial-tasks.md | 157 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) diff --git a/doc/tutorial-tasks.md b/doc/tutorial-tasks.md index f81311d89a3..405f4ac7347 100644 --- a/doc/tutorial-tasks.md +++ b/doc/tutorial-tasks.md @@ -295,6 +295,163 @@ let result = ports.foldl(0, |accum, port| *accum + port.recv() ); # fn some_expensive_computation(_i: uint) -> int { 42 } ~~~ +# TODO + +# Handling task failure + +Rust has a built-in mechanism for raising exceptions, written `fail` +(or `fail ~"reason"`, or sometimes `assert expr`), and it causes the +task to unwind its stack, running destructors and freeing memory along +the way, and then exit itself. Unlike C++, exceptions in Rust are +unrecoverable within a single task - once a task fails there is no way +to "catch" the exception. + +All tasks are, by default, _linked_ to each other, meaning their fate +is interwined, and if one fails so do all of them. + +~~~ +# use task::spawn; +# fn do_some_work() { loop { task::yield() } } +# do task::try { +// Create a child task that fails +do spawn { fail } + +// This will also fail because the task we spawned failed +do_some_work(); +# }; +~~~ + +While it isn't possible for a task to recover from failure, +tasks may be notified when _other_ tasks fail. The simplest way +of handling task failure is with the `try` function, which is +similar to spawn, but immediately blocks waiting for the child +task to finish. + +~~~ +# fn some_condition() -> bool { false } +# fn calculate_result() -> int { 0 } +let result: Result = do task::try { + if some_condition() { + calculate_result() + } else { + fail ~"oops!"; + } +}; +assert result.is_err(); +~~~ + +Unlike `spawn`, the function spawned using `try` may return a value, +which `try` will dutifully propagate back to the caller in a [`Result`] +enum. If the child task terminates successfully, `try` will +return an `Ok` result; if the child task fails, `try` will return +an `Error` result. + +[`Result`]: core/result.html + +> ***Note:*** A failed task does not currently produce a useful error +> value (all error results from `try` are equal to `Err(())`). In the +> future it may be possible for tasks to intercept the value passed to +> `fail`. + +TODO: Need discussion of `future_result` in order to make failure +modes useful. + +But not all failure is created equal. In some cases you might need to +abort the entire program (perhaps you're writing an assert which, if +it trips, indicates an unrecoverable logic error); in other cases you +might want to contain the failure at a certain boundary (perhaps a +small piece of input from the outside world, which you happen to be +processing in parallel, is malformed and its processing task can't +proceed). Hence the need for different _linked failure modes_. + +## Failure modes + +By default, task failure is _bidirectionally linked_, which means if +either task dies, it kills the other one. + +~~~ +# fn sleep_forever() { loop { task::yield() } } +# do task::try { +do task::spawn { + do task::spawn { + fail; // All three tasks will die. + } + sleep_forever(); // Will get woken up by force, then fail +} +sleep_forever(); // Will get woken up by force, then fail +# }; +~~~ + +If you want parent tasks to kill their children, but not for a child +task's failure to kill the parent, you can call +`task::spawn_supervised` for _unidirectionally linked_ failure. The +function `task::try`, which we saw previously, uses `spawn_supervised` +internally, with additional logic to wait for the child task to finish +before returning. Hence: + +~~~ +# use pipes::{stream, Chan, Port}; +# use task::{spawn, try}; +# fn sleep_forever() { loop { task::yield() } } +# do task::try { +let (sender, receiver): (Chan, Port) = stream(); +do spawn { // Bidirectionally linked + // Wait for the supervised child task to exist. + let message = receiver.recv(); + // Kill both it and the parent task. + assert message != 42; +} +do try { // Unidirectionally linked + sender.send(42); + sleep_forever(); // Will get woken up by force +} +// Flow never reaches here -- parent task was killed too. +# }; +~~~ + +Supervised failure is useful in any situation where one task manages +multiple fallible child tasks, and the parent task can recover +if any child files. On the other hand, if the _parent_ (supervisor) fails +then there is nothing the children can do to recover, so they should +also fail. + +Supervised task failure propagates across multiple generations even if +an intermediate generation has already exited: + +~~~ +# fn sleep_forever() { loop { task::yield() } } +# fn wait_for_a_while() { for 1000.times { task::yield() } } +# do task::try:: { +do task::spawn_supervised { + do task::spawn_supervised { + sleep_forever(); // Will get woken up by force, then fail + } + // Intermediate task immediately exits +} +wait_for_a_while(); +fail; // Will kill grandchild even if child has already exited +# }; +~~~ + +Finally, tasks can be configured to not propagate failure to each +other at all, using `task::spawn_unlinked` for _isolated failure_. + +~~~ +# fn random() -> int { 100 } +# fn sleep_for(i: int) { for i.times { task::yield() } } +# do task::try::<()> { +let (time1, time2) = (random(), random()); +do task::spawn_unlinked { + sleep_for(time2); // Won't get forced awake + fail; +} +sleep_for(time1); // Won't get forced awake +fail; +// It will take MAX(time1,time2) for the program to finish. +# }; +~~~ + + # Unfinished notes ## Actor patterns