From b680593184e4df8ce1f7aa8b9d9919673a7b400b Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 13 Jul 2018 00:11:13 -0500 Subject: [PATCH 01/10] =?UTF-8?q?Clarify=20what=20=E2=80=9Csession?= =?UTF-8?q?=E2=80=9D=20means=20in=20the=20kernel=20message=20headers=20fro?= =?UTF-8?q?m=20the=20client=20or=20the=20kernel.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/messaging.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/messaging.rst b/docs/messaging.rst index 7c533a7de..b313c895e 100644 --- a/docs/messaging.rst +++ b/docs/messaging.rst @@ -122,6 +122,14 @@ A message is defined by the following four-dictionary structure:: 'buffers': list, } +.. note:: + +A client session value, in message headers from a client, should be unique among +all clients connected to a kernel and should be constant over the lifetime of +the client. A kernel session value, in message headers from a kernel, should be +generated on kernel startup or restart and should be constant for the lifetime +of the kernel. + .. versionchanged:: 5.0 ``version`` key added to the header. From 49aabf9419995e1de53932047433624e7dcd248c Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 13 Jul 2018 00:31:08 -0500 Subject: [PATCH 02/10] Clarify the implied required busy/idle messages. These messages were actually required, but you had to read the status message documentation to realize they were required. This makes the requirement more explicit. --- docs/messaging.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/messaging.rst b/docs/messaging.rst index b313c895e..c76d452af 100644 --- a/docs/messaging.rst +++ b/docs/messaging.rst @@ -147,8 +147,9 @@ Compatibility ============= Kernels must implement the :ref:`execute ` and :ref:`kernel info -` messages in order to be usable. All other message types -are optional, although we recommend implementing :ref:`completion +` messages, along with the associated busy and idle +:ref:`status` messages. All other message types are +optional, although we recommend implementing :ref:`completion ` if possible. Kernels do not need to send any reply for messages they don't handle, and frontends should provide sensible behaviour if no reply arrives (except for the required execution and kernel info messages). From 9dabc43b110a55ce72408d2d3fa73920de94c49b Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 13 Jul 2018 09:45:49 -0500 Subject: [PATCH 03/10] Deprecate sending the shutdown_request message on the shell channel. In conversation with @minrk, we decided shutdown messages should always be treated with a higher priority, and it was confusing to have them sent on either channel. --- docs/messaging.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/messaging.rst b/docs/messaging.rst index c76d452af..d39c4af46 100644 --- a/docs/messaging.rst +++ b/docs/messaging.rst @@ -943,8 +943,7 @@ multiple cases: The client sends a shutdown request to the kernel, and once it receives the reply message (which is otherwise empty), it can assume that the kernel has -completed shutdown safely. The request can be sent on either the `control` or -`shell` channels. +completed shutdown safely. The request is sent on the `control` channel. Upon their own shutdown, client applications will typically execute a last minute sanity check and forcefully terminate any kernel that is still alive, to @@ -968,6 +967,12 @@ Message type: ``shutdown_reply``:: socket, they simply send a forceful process termination signal, since a dead process is unlikely to respond in any useful way to messages. +.. versionchanged:: 5.4 + + Sending a ``shutdown_request`` message on the ``shell`` channel is deprecated. + + + .. _msging_interrupt: Kernel interrupt From 4f7c8256094252a75d78eedbe8990642e66ec56e Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 13 Jul 2018 09:49:30 -0500 Subject: [PATCH 04/10] Clarify the status message. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Make the ‘starting’ status optional, since receiving it is unreliable anyway due to race conditions with connecting to the kernel. 2. Clarify that the kernel may send other status states that could be ignored 3. Take out the notebook-specific states, since that should be in notebook documentation, not in kernel messaging docs. --- docs/messaging.rst | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/docs/messaging.rst b/docs/messaging.rst index d39c4af46..96cc6507a 100644 --- a/docs/messaging.rst +++ b/docs/messaging.rst @@ -1205,6 +1205,8 @@ Message type: ``error``:: ``pyerr`` renamed to ``error`` +.. _status: + Kernel status ------------- @@ -1215,8 +1217,7 @@ Message type: ``status``:: content = { # When the kernel starts to handle a message, it will enter the 'busy' # state and when it finishes, it will enter the 'idle' state. - # The kernel will publish state 'starting' exactly once at process startup. - execution_state : ('busy', 'idle', 'starting') + execution_state : ('busy', 'idle', other optional states) } When a kernel receives a request and begins processing it, @@ -1227,6 +1228,10 @@ it shall publish a status message with ``execution_state: 'idle'``. Thus, the outputs associated with a given execution shall generally arrive between the busy and idle status messages associated with a given request. +A kernel may send optional status messages with execution states other than +`busy` or `idle`. For example, a kernel may send a status message with a +`starting` execution state exactly once at process startup. + .. note:: **A caveat for asynchronous output** @@ -1243,14 +1248,6 @@ between the busy and idle status messages associated with a given request. Busy and idle messages should be sent before/after handling every request, not just execution. -.. note:: - - Extra status messages are added between the notebook webserver and websocket clients - that are not sent by the kernel. These are: - - - restarting (kernel has died, but will be automatically restarted) - - dead (kernel has died, restarting has failed) - Clear output ------------ From e899ef1043dee6367e060a7d30f56a487b1ba0fd Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 18 Jul 2018 13:27:51 -0700 Subject: [PATCH 05/10] Send the shutdown message over the control channel in client.py. --- jupyter_client/client.py | 33 +++++++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/jupyter_client/client.py b/jupyter_client/client.py index 763af85a7..a20673be2 100644 --- a/jupyter_client/client.py +++ b/jupyter_client/client.py @@ -35,12 +35,13 @@ def validate_string_dict(dct): class KernelClient(ConnectionFileMixin): """Communicates with a single kernel on any host via zmq channels. - There are four channels associated with each kernel: + There are five channels associated with each kernel: * shell: for request/reply calls to the kernel. * iopub: for the kernel to publish results to frontends. * hb: for monitoring the kernel's heartbeat. * stdin: for frontends to reply to raw_input calls in the kernel. + * control: for kernel management calls to the kernel. The messages that can be sent on these channels are exposed as methods of the client (KernelClient.execute, complete, history, etc.). These methods only @@ -58,12 +59,14 @@ def _context_default(self): iopub_channel_class = Type(ChannelABC) stdin_channel_class = Type(ChannelABC) hb_channel_class = Type(HBChannelABC) + control_channel_class = Type(ChannelABC) # Protected traits _shell_channel = Any() _iopub_channel = Any() _stdin_channel = Any() _hb_channel = Any() + _control_channel = Any() # flag for whether execute requests should be allowed to call raw_input: allow_stdin = True @@ -84,11 +87,15 @@ def get_stdin_msg(self, *args, **kwargs): """Get a message from the stdin channel""" return self.stdin_channel.get_msg(*args, **kwargs) + def get_control_msg(self, *args, **kwargs): + """Get a message from the control channel""" + return self.control_channel.get_msg(*args, **kwargs) + #-------------------------------------------------------------------------- # Channel management methods #-------------------------------------------------------------------------- - def start_channels(self, shell=True, iopub=True, stdin=True, hb=True): + def start_channels(self, shell=True, iopub=True, stdin=True, hb=True, control=True): """Starts the channels for this kernel. This will create the channels if they do not exist and then start @@ -109,6 +116,9 @@ def start_channels(self, shell=True, iopub=True, stdin=True, hb=True): self.allow_stdin = False if hb: self.hb_channel.start() + if control: + self.control_channel.start() + self.kernel_info() def stop_channels(self): """Stops all the running channels for this kernel. @@ -123,12 +133,15 @@ def stop_channels(self): self.stdin_channel.stop() if self.hb_channel.is_alive(): self.hb_channel.stop() + if self.control_channel.is_alive(): + self.control_channel.stop() @property def channels_running(self): """Are any of the channels created and running?""" return (self.shell_channel.is_alive() or self.iopub_channel.is_alive() or - self.stdin_channel.is_alive() or self.hb_channel.is_alive()) + self.stdin_channel.is_alive() or self.hb_channel.is_alive() or + self.control_channel.is_alive()) ioloop = None # Overridden in subclasses that use pyzmq event loop @@ -179,6 +192,18 @@ def hb_channel(self): ) return self._hb_channel + @property + def control_channel(self): + """Get the control channel object for this kernel.""" + if self._control_channel is None: + url = self._make_url('control') + self.log.debug("connecting control channel to %s", url) + socket = self.connect_control(identity=self.session.bsession) + self._control_channel = self.control_channel_class( + socket, self.session, self.ioloop + ) + return self._control_channel + def is_alive(self): """Is the kernel process still running?""" from .manager import KernelManager @@ -401,7 +426,7 @@ def shutdown(self, restart=False): # Send quit message to kernel. Once we implement kernel-side setattr, # this should probably be done that way, but for now this will do. msg = self.session.msg('shutdown_request', {'restart':restart}) - self.shell_channel.send(msg) + self.control_channel.send(msg) return msg['header']['msg_id'] def is_complete(self, code): From c4d2414fc8658401446918ad19f3887254221d7b Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Wed, 18 Jul 2018 14:03:56 -0700 Subject: [PATCH 06/10] Clarify the session ids in message headers. From conversations with @minrk. --- docs/messaging.rst | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/messaging.rst b/docs/messaging.rst index 96cc6507a..77ab743ac 100644 --- a/docs/messaging.rst +++ b/docs/messaging.rst @@ -124,15 +124,26 @@ A message is defined by the following four-dictionary structure:: .. note:: -A client session value, in message headers from a client, should be unique among -all clients connected to a kernel and should be constant over the lifetime of -the client. A kernel session value, in message headers from a kernel, should be -generated on kernel startup or restart and should be constant for the lifetime -of the kernel. + The ``session`` id in a message header identifies a unique entity with state, + such as a kernel process or client process. + + A client session id, in message headers from a client, should be unique among + all clients connected to a kernel. When a client reconnects to a kernel, it + should use the same client session id in its message headers. When a client + restarts, it should generate a new client session id. + + A kernel session id, in message headers from a kernel, should identify a + particular kernel process. If a kernel is restarted, the kernel session id + should be regenerated. + + The session id in a message header can be used to identify the sending entity. + For example, if a client disconnects and reconnects to a kernel, and messages + from the kernel have a different kernel session id than prior to the disconnect, + the client should assume that the kernel was restarted. .. versionchanged:: 5.0 - ``version`` key added to the header. + ``version`` key added to the header. .. versionchanged:: 5.1 From c369247f4e90d2293055b17c9a4ca00a7e8a6add Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 19 Jul 2018 02:32:04 -0700 Subject: [PATCH 07/10] Fix control channel copy/paste error. --- jupyter_client/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/jupyter_client/client.py b/jupyter_client/client.py index a20673be2..ef599b00f 100644 --- a/jupyter_client/client.py +++ b/jupyter_client/client.py @@ -118,7 +118,6 @@ def start_channels(self, shell=True, iopub=True, stdin=True, hb=True, control=Tr self.hb_channel.start() if control: self.control_channel.start() - self.kernel_info() def stop_channels(self): """Stops all the running channels for this kernel. From 846354e8758e8a71d409eb73bf2bf7d6c41516bf Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 19 Jul 2018 02:32:37 -0700 Subject: [PATCH 08/10] Move shutdown request to its own section to emphasize the message is on the control channel. --- jupyter_client/client.py | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/jupyter_client/client.py b/jupyter_client/client.py index ef599b00f..01a558f55 100644 --- a/jupyter_client/client.py +++ b/jupyter_client/client.py @@ -407,8 +407,24 @@ def _handle_kernel_info_reply(self, msg): if adapt_version != major_protocol_version: self.session.adapt_version = adapt_version + def is_complete(self, code): + """Ask the kernel whether some code is complete and ready to execute.""" + msg = self.session.msg('is_complete_request', {'code': code}) + self.shell_channel.send(msg) + return msg['header']['msg_id'] + + def input(self, string): + """Send a string of raw input to the kernel. + + This should only be called in response to the kernel sending an + ``input_request`` message on the stdin channel. + """ + content = dict(value=string) + msg = self.session.msg('input_reply', content) + self.stdin_channel.send(msg) + def shutdown(self, restart=False): - """Request an immediate kernel shutdown. + """Request an immediate kernel shutdown on the control channel. Upon receipt of the (empty) reply, client code can safely assume that the kernel has shut down and it's safe to forcefully terminate it if @@ -428,21 +444,4 @@ def shutdown(self, restart=False): self.control_channel.send(msg) return msg['header']['msg_id'] - def is_complete(self, code): - """Ask the kernel whether some code is complete and ready to execute.""" - msg = self.session.msg('is_complete_request', {'code': code}) - self.shell_channel.send(msg) - return msg['header']['msg_id'] - - def input(self, string): - """Send a string of raw input to the kernel. - - This should only be called in response to the kernel sending an - ``input_request`` message on the stdin channel. - """ - content = dict(value=string) - msg = self.session.msg('input_reply', content) - self.stdin_channel.send(msg) - - KernelClientABC.register(KernelClient) From 5bac5e511ddefc75acc8dffa6d43c8a614cd7a08 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Thu, 19 Jul 2018 02:33:41 -0700 Subject: [PATCH 09/10] Percolate a control connection throughout the code. --- jupyter_client/blocking/client.py | 17 ++++++++++++----- jupyter_client/clientabc.py | 10 +++++++++- jupyter_client/connect.py | 10 +++++----- jupyter_client/consoleapp.py | 9 +++++++-- jupyter_client/manager.py | 2 +- jupyter_client/threaded.py | 5 +++-- 6 files changed, 37 insertions(+), 16 deletions(-) diff --git a/jupyter_client/blocking/client.py b/jupyter_client/blocking/client.py index c0196ba36..87e0e769e 100644 --- a/jupyter_client/blocking/client.py +++ b/jupyter_client/blocking/client.py @@ -36,7 +36,7 @@ TimeoutError = RuntimeError -def reqrep(meth): +def reqrep(meth, channel='shell'): def wrapped(self, *args, **kwargs): reply = kwargs.pop('reply', False) timeout = kwargs.pop('timeout', None) @@ -44,7 +44,7 @@ def wrapped(self, *args, **kwargs): if not reply: return msg_id - return self._recv_reply(msg_id, timeout=timeout) + return self._recv_reply(msg_id, timeout=timeout, channel=channel) if not meth.__doc__: # python -OO removes docstrings, @@ -135,9 +135,10 @@ def wait_for_ready(self, timeout=None): iopub_channel_class = Type(ZMQSocketChannel) stdin_channel_class = Type(ZMQSocketChannel) hb_channel_class = Type(HBChannel) + control_channel_class = Type(ZMQSocketChannel) - def _recv_reply(self, msg_id, timeout=None): + def _recv_reply(self, msg_id, timeout=None, channel='shell'): """Receive and return the reply for a given request""" if timeout is not None: deadline = monotonic() + timeout @@ -145,7 +146,10 @@ def _recv_reply(self, msg_id, timeout=None): if timeout is not None: timeout = max(0, deadline - monotonic()) try: - reply = self.get_shell_msg(timeout=timeout) + if channel == 'control': + reply = self.get_control_msg(timeout=timeout) + else: + reply = self.get_shell_msg(timeout=timeout) except Empty: raise TimeoutError("Timeout waiting for reply") if reply['parent_header'].get('msg_id') != msg_id: @@ -154,13 +158,16 @@ def _recv_reply(self, msg_id, timeout=None): return reply + # replies come on the shell channel execute = reqrep(KernelClient.execute) history = reqrep(KernelClient.history) complete = reqrep(KernelClient.complete) inspect = reqrep(KernelClient.inspect) kernel_info = reqrep(KernelClient.kernel_info) comm_info = reqrep(KernelClient.comm_info) - shutdown = reqrep(KernelClient.shutdown) + + # replies come on the control channel + shutdown = reqrep(KernelClient.shutdown, channel='control') def _stdin_hook_default(self, msg): diff --git a/jupyter_client/clientabc.py b/jupyter_client/clientabc.py index 7a718284a..9a47d2fcb 100644 --- a/jupyter_client/clientabc.py +++ b/jupyter_client/clientabc.py @@ -47,12 +47,16 @@ def hb_channel_class(self): def stdin_channel_class(self): pass + @abc.abstractproperty + def control_channel_class(self): + pass + #-------------------------------------------------------------------------- # Channel management methods #-------------------------------------------------------------------------- @abc.abstractmethod - def start_channels(self, shell=True, iopub=True, stdin=True, hb=True): + def start_channels(self, shell=True, iopub=True, stdin=True, hb=True, control=True): pass @abc.abstractmethod @@ -78,3 +82,7 @@ def stdin_channel(self): @abc.abstractproperty def hb_channel(self): pass + + @abc.abstractproperty + def control_channel(self): + pass diff --git a/jupyter_client/connect.py b/jupyter_client/connect.py index 2cc89de35..81d6d852a 100644 --- a/jupyter_client/connect.py +++ b/jupyter_client/connect.py @@ -226,7 +226,7 @@ def find_connection_file(filename='kernel-*.json', path=None, profile=None): def tunnel_to_kernel(connection_info, sshserver, sshkey=None): """tunnel connections to a kernel via ssh - This will open four SSH tunnels from localhost on this machine to the + This will open five SSH tunnels from localhost on this machine to the ports associated with the kernel. They can be either direct localhost-localhost tunnels, or if an intermediate server is necessary, the kernel must be listening on a public IP. @@ -246,8 +246,8 @@ def tunnel_to_kernel(connection_info, sshserver, sshkey=None): Returns ------- - (shell, iopub, stdin, hb) : ints - The four ports on localhost that have been forwarded to the kernel. + (shell, iopub, stdin, hb, control) : ints + The five ports on localhost that have been forwarded to the kernel. """ from .ssh import tunnel if isinstance(connection_info, string_types): @@ -257,8 +257,8 @@ def tunnel_to_kernel(connection_info, sshserver, sshkey=None): cf = connection_info - lports = tunnel.select_random_ports(4) - rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port'] + lports = tunnel.select_random_ports(5) + rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port'], cf['control_port'] remote_ip = cf['ip'] diff --git a/jupyter_client/consoleapp.py b/jupyter_client/consoleapp.py index 2e27a2918..e8c8e769e 100644 --- a/jupyter_client/consoleapp.py +++ b/jupyter_client/consoleapp.py @@ -72,6 +72,7 @@ shell = 'JupyterConsoleApp.shell_port', iopub = 'JupyterConsoleApp.iopub_port', stdin = 'JupyterConsoleApp.stdin_port', + control = 'JupyterConsoleApp.control_port', existing = 'JupyterConsoleApp.existing', f = 'JupyterConsoleApp.connection_file', @@ -222,7 +223,8 @@ def init_ssh(self): shell_port=self.shell_port, iopub_port=self.iopub_port, stdin_port=self.stdin_port, - hb_port=self.hb_port + hb_port=self.hb_port, + control_port=self.control_port ) self.log.info("Forwarding connections to %s via %s"%(ip, self.sshserver)) @@ -236,7 +238,7 @@ def init_ssh(self): self.log.error("Could not setup tunnels", exc_info=True) self.exit(1) - self.shell_port, self.iopub_port, self.stdin_port, self.hb_port = newports + self.shell_port, self.iopub_port, self.stdin_port, self.hb_port, self.control_port = newports cf = self.connection_file root, ext = os.path.splitext(cf) @@ -275,6 +277,7 @@ def init_kernel_manager(self): iopub_port=self.iopub_port, stdin_port=self.stdin_port, hb_port=self.hb_port, + control_port=self.control_port, connection_file=self.connection_file, kernel_name=self.kernel_name, parent=self, @@ -302,6 +305,7 @@ def init_kernel_manager(self): self.iopub_port=km.iopub_port self.stdin_port=km.stdin_port self.hb_port=km.hb_port + self.control_port=km.control_port self.connection_file = km.connection_file atexit.register(self.kernel_manager.cleanup_connection_file) @@ -318,6 +322,7 @@ def init_kernel_client(self): iopub_port=self.iopub_port, stdin_port=self.stdin_port, hb_port=self.hb_port, + control_port=self.control_port, connection_file=self.connection_file, parent=self, ) diff --git a/jupyter_client/manager.py b/jupyter_client/manager.py index bf94ad002..05b1bb53d 100644 --- a/jupyter_client/manager.py +++ b/jupyter_client/manager.py @@ -297,7 +297,7 @@ def shutdown_kernel(self, now=False, restart=False): This attempts to shutdown the kernels cleanly by: - 1. Sending it a shutdown message over the shell channel. + 1. Sending it a shutdown message over the control channel. 2. If that fails, the kernel is shutdown forcibly by sending it a signal. diff --git a/jupyter_client/threaded.py b/jupyter_client/threaded.py index 83a6ad0eb..801ac7acc 100644 --- a/jupyter_client/threaded.py +++ b/jupyter_client/threaded.py @@ -230,14 +230,14 @@ def ioloop(self): ioloop_thread = Instance(IOLoopThread, allow_none=True) - def start_channels(self, shell=True, iopub=True, stdin=True, hb=True): + def start_channels(self, shell=True, iopub=True, stdin=True, hb=True, control=True): self.ioloop_thread = IOLoopThread() self.ioloop_thread.start() if shell: self.shell_channel._inspect = self._check_kernel_info_reply - super(ThreadedKernelClient, self).start_channels(shell, iopub, stdin, hb) + super(ThreadedKernelClient, self).start_channels(shell, iopub, stdin, hb, control) def _check_kernel_info_reply(self, msg): """This is run in the ioloop thread when the kernel info reply is received @@ -255,3 +255,4 @@ def stop_channels(self): shell_channel_class = Type(ThreadedZMQSocketChannel) stdin_channel_class = Type(ThreadedZMQSocketChannel) hb_channel_class = Type(HBChannel) + control_channel_class = Type(ThreadedZMQSocketChannel) From 5f0a21b5cae780fa33b2a92cb6743a2f9d0949d8 Mon Sep 17 00:00:00 2001 From: Jason Grout Date: Fri, 26 Jul 2019 06:32:20 -0700 Subject: [PATCH 10/10] Revert status message updates to revert back to exactly three status message states. We still remove the part about spoofed status messages since it really is not part of the kernel message spec. --- docs/messaging.rst | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/messaging.rst b/docs/messaging.rst index 77ab743ac..7eb6435a6 100644 --- a/docs/messaging.rst +++ b/docs/messaging.rst @@ -1228,7 +1228,8 @@ Message type: ``status``:: content = { # When the kernel starts to handle a message, it will enter the 'busy' # state and when it finishes, it will enter the 'idle' state. - execution_state : ('busy', 'idle', other optional states) + # The kernel will publish state 'starting' exactly once at process startup. + execution_state : ('busy', 'idle', 'starting') } When a kernel receives a request and begins processing it, @@ -1239,10 +1240,6 @@ it shall publish a status message with ``execution_state: 'idle'``. Thus, the outputs associated with a given execution shall generally arrive between the busy and idle status messages associated with a given request. -A kernel may send optional status messages with execution states other than -`busy` or `idle`. For example, a kernel may send a status message with a -`starting` execution state exactly once at process startup. - .. note:: **A caveat for asynchronous output**