🚨 Errors and Shutdown¶
This page shows what happens when a hook of a node raises, where the host receives the exception, and the timeouts of the host's calls.
One node's exception stops everything¶
Whichever hook of whichever node raises (on_update, on_start, state_dict or any other), the whole run stops:
- Mujou logs the node's path, the hook and the traceback to the logger
"mujou"at the ERROR level - Each node finishes the hook it is running, stops, and calls its
on_exception(), without waiting for the others. The nodes that did not raise are called too - Once every
on_exception()has returned, every node'son_stop()is called, the one that raised included - The host gets a
NetworkError, a subclass ofRuntimeError, whose__cause__is the first exception. Exceptions of other nodes at about the same time are only logged
Mujou never stops only the node that raised: its neighbors would keep reading its old values and drift away from the equilibrium without anyone noticing.
"""What happens when a node raises.
The whole run stops: every node's ``on_exception``, then its ``on_stop``, is
called, and the host gets a ``NetworkError`` whose ``__cause__`` is the node's
exception. An exception in ``on_start`` comes from ``mujou.run`` itself.
"""
import logging
import sys
from typing import override
import torch
import mujou
from mujou import NetworkError, Node, Port, TensorSpec
SCALAR = TensorSpec(())
# mujou also logs the node's exception, with its traceback, to the "mujou" logger.
# Show the log on stdout here (without a handler, Python prints it to stderr).
logging.basicConfig(stream=sys.stdout, format="log: %(message)s")
class Checked(Node):
"""Passes its input on, and raises once it is negative."""
def __init__(self, fail_on_start: bool = False) -> None:
super().__init__()
self.fail_on_start = fail_on_start
self.prev = Port(recv=SCALAR)
self.next = Port(send=SCALAR)
@override
def on_start(self) -> None:
super().on_start()
if self.fail_on_start:
raise RuntimeError("cannot start")
@override
def on_update(self, dt: float) -> None:
super().on_update(dt)
u = self.prev.recv()
if u < 0:
raise ValueError(f"negative input: {u.item()}")
self.next.send(u)
@override
def on_exception(self) -> None:
super().on_exception()
print("on_exception: called on every node")
@override
def on_stop(self) -> None:
super().on_stop()
print("on_stop: called after on_exception")
class Single(mujou.Network):
def __init__(self, fail_on_start: bool = False) -> None:
super().__init__()
self.checked = Checked(fail_on_start)
self.prev = self.checked.prev
# An exception while the run goes on reaches the host at its next call.
net = Single()
run = mujou.run(net)
run.write({net.prev: torch.tensor(-1.0)})
try:
run.join(timeout=10.0) # waits until the run stops (here: by the exception)
except NetworkError as error:
cause = error.__cause__
print(f"NetworkError, caused by {type(cause).__name__}: {cause}")
assert isinstance(cause, ValueError)
# An exception in on_start comes from mujou.run.
try:
mujou.run(Single(fail_on_start=True))
except NetworkError as error:
print(f"mujou.run raised NetworkError, caused by {error.__cause__!r}")
assert isinstance(error.__cause__, RuntimeError)
Where the host receives it¶
| The exception happens | What raises NetworkError |
|---|---|
While the run starts (on_start, ...) |
mujou.run itself, after the ending work (on_exception, on_stop) |
| After the start | The next write, read, read_with_time, subscribe (and its iterator), wait_for, state_dict, shutdown or join, or step of a ManualRun |
An exception after the start reaches the host at its next call, so a host that only waits does not notice it: wait with run.join(timeout), not with run.timer.sleep. At the end of a with block, the shutdown raises it; if the body of the block raises too, the body's exception goes on and the NetworkError is dropped (the log still has the node's exception).
on_exception and on_stop¶
| Hook | Called | What to write |
|---|---|---|
on_exception() |
When the run ends by an exception, on every node, first | The work of an abnormal end, such as a safe stop of the actuators |
on_stop() |
Once at every end, normal or not | The release of resources such as I/O; not a change of the state |
on_exceptiontakes no arguments: a node does not learn which exception stopped the run. The host handles the cause, from__cause__- To handle an exception within a node, catch it with
tryin the hook: an exception caught there does not stop the run - If
on_exceptionoron_stopraises, the exception is logged, and the ending goes on self.timerdoes not move in these hooks: see the rules a node keeps
Logging¶
The node's exception goes to the logger "mujou" with its traceback; a program that has not configured logging prints it to standard error. Configure Python's logging to send it elsewhere:
# mujou also logs the node's exception, with its traceback, to the "mujou" logger.
# Show the log on stdout here (without a handler, Python prints it to stderr).
logging.basicConfig(stream=sys.stdout, format="log: %(message)s")
The state after an exception¶
The state of a run that ended by an exception is not kept: run.state_dict() raises NetworkError too. Take the state you want to keep while the network runs, regularly during a long run. To run again, build new instances.
Timeouts¶
shutdown, join, subscribe, wait_for and state_dict take a timeout in real seconds; the default math.inf waits as long as it takes.
| Call | When the time runs out |
|---|---|
run.shutdown(timeout=...) |
TimeoutError. The request to stop stands, and the nodes update no more; the ending goes on, and join waits for it |
run.join(timeout=...) |
TimeoutError. Nothing changes |
run.subscribe(port, timeout=...) |
TimeoutError from the iterator, counting the whole iteration, the body of the loop included. The run keeps running |
run.wait_for(port, condition, timeout=...) |
TimeoutError. The run keeps running |
run.state_dict(timeout=...) |
TimeoutError, while some node has not paused (each finishes its on_update first). No state is taken, and the run keeps running |
A node whose single on_update is long (heavy BP, say) delays every pause for the state.
Interpreter exit¶
If the interpreter exits while a run is running, Mujou shuts the run down and waits for every on_stop, once the other non-daemon threads have ended. A thread that iterates over subscribe keeps the exit waiting until its run ends, so end each run yourself, with shutdown or a with block.