Skip to content

Commit 4def2a2

Browse files
Merge pull request #388 from jasongrout/update
Updates to the kernel messaging spec
2 parents 4738e6a + 5f0a21b commit 4def2a2

File tree

8 files changed

+114
-51
lines changed

8 files changed

+114
-51
lines changed

docs/messaging.rst

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -122,9 +122,28 @@ A message is defined by the following four-dictionary structure::
122122
'buffers': list,
123123
}
124124

125+
.. note::
126+
127+
The ``session`` id in a message header identifies a unique entity with state,
128+
such as a kernel process or client process.
129+
130+
A client session id, in message headers from a client, should be unique among
131+
all clients connected to a kernel. When a client reconnects to a kernel, it
132+
should use the same client session id in its message headers. When a client
133+
restarts, it should generate a new client session id.
134+
135+
A kernel session id, in message headers from a kernel, should identify a
136+
particular kernel process. If a kernel is restarted, the kernel session id
137+
should be regenerated.
138+
139+
The session id in a message header can be used to identify the sending entity.
140+
For example, if a client disconnects and reconnects to a kernel, and messages
141+
from the kernel have a different kernel session id than prior to the disconnect,
142+
the client should assume that the kernel was restarted.
143+
125144
.. versionchanged:: 5.0
126145

127-
``version`` key added to the header.
146+
``version`` key added to the header.
128147

129148
.. versionchanged:: 5.1
130149

@@ -139,8 +158,9 @@ Compatibility
139158
=============
140159

141160
Kernels must implement the :ref:`execute <execute>` and :ref:`kernel info
142-
<msging_kernel_info>` messages in order to be usable. All other message types
143-
are optional, although we recommend implementing :ref:`completion
161+
<msging_kernel_info>` messages, along with the associated busy and idle
162+
:ref:`status` messages. All other message types are
163+
optional, although we recommend implementing :ref:`completion
144164
<msging_completion>` if possible. Kernels do not need to send any reply for
145165
messages they don't handle, and frontends should provide sensible behaviour if
146166
no reply arrives (except for the required execution and kernel info messages).
@@ -940,8 +960,7 @@ multiple cases:
940960

941961
The client sends a shutdown request to the kernel, and once it receives the
942962
reply message (which is otherwise empty), it can assume that the kernel has
943-
completed shutdown safely. The request can be sent on either the `control` or
944-
`shell` channels.
963+
completed shutdown safely. The request is sent on the `control` channel.
945964

946965
Upon their own shutdown, client applications will typically execute a last
947966
minute sanity check and forcefully terminate any kernel that is still alive, to
@@ -965,6 +984,12 @@ Message type: ``shutdown_reply``::
965984
socket, they simply send a forceful process termination signal, since a dead
966985
process is unlikely to respond in any useful way to messages.
967986

987+
.. versionchanged:: 5.4
988+
989+
Sending a ``shutdown_request`` message on the ``shell`` channel is deprecated.
990+
991+
992+
968993
.. _msging_interrupt:
969994

970995
Kernel interrupt
@@ -1197,6 +1222,8 @@ Message type: ``error``::
11971222

11981223
``pyerr`` renamed to ``error``
11991224

1225+
.. _status:
1226+
12001227
Kernel status
12011228
-------------
12021229

@@ -1235,14 +1262,6 @@ between the busy and idle status messages associated with a given request.
12351262
Busy and idle messages should be sent before/after handling every request,
12361263
not just execution.
12371264

1238-
.. note::
1239-
1240-
Extra status messages are added between the notebook webserver and websocket clients
1241-
that are not sent by the kernel. These are:
1242-
1243-
- restarting (kernel has died, but will be automatically restarted)
1244-
- dead (kernel has died, restarting has failed)
1245-
12461265
Clear output
12471266
------------
12481267

jupyter_client/blocking/client.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@
3636
TimeoutError = RuntimeError
3737

3838

39-
def reqrep(meth):
39+
def reqrep(meth, channel='shell'):
4040
def wrapped(self, *args, **kwargs):
4141
reply = kwargs.pop('reply', False)
4242
timeout = kwargs.pop('timeout', None)
4343
msg_id = meth(self, *args, **kwargs)
4444
if not reply:
4545
return msg_id
4646

47-
return self._recv_reply(msg_id, timeout=timeout)
47+
return self._recv_reply(msg_id, timeout=timeout, channel=channel)
4848

4949
if not meth.__doc__:
5050
# python -OO removes docstrings,
@@ -135,17 +135,21 @@ def wait_for_ready(self, timeout=None):
135135
iopub_channel_class = Type(ZMQSocketChannel)
136136
stdin_channel_class = Type(ZMQSocketChannel)
137137
hb_channel_class = Type(HBChannel)
138+
control_channel_class = Type(ZMQSocketChannel)
138139

139140

140-
def _recv_reply(self, msg_id, timeout=None):
141+
def _recv_reply(self, msg_id, timeout=None, channel='shell'):
141142
"""Receive and return the reply for a given request"""
142143
if timeout is not None:
143144
deadline = monotonic() + timeout
144145
while True:
145146
if timeout is not None:
146147
timeout = max(0, deadline - monotonic())
147148
try:
148-
reply = self.get_shell_msg(timeout=timeout)
149+
if channel == 'control':
150+
reply = self.get_control_msg(timeout=timeout)
151+
else:
152+
reply = self.get_shell_msg(timeout=timeout)
149153
except Empty:
150154
raise TimeoutError("Timeout waiting for reply")
151155
if reply['parent_header'].get('msg_id') != msg_id:
@@ -154,13 +158,16 @@ def _recv_reply(self, msg_id, timeout=None):
154158
return reply
155159

156160

161+
# replies come on the shell channel
157162
execute = reqrep(KernelClient.execute)
158163
history = reqrep(KernelClient.history)
159164
complete = reqrep(KernelClient.complete)
160165
inspect = reqrep(KernelClient.inspect)
161166
kernel_info = reqrep(KernelClient.kernel_info)
162167
comm_info = reqrep(KernelClient.comm_info)
163-
shutdown = reqrep(KernelClient.shutdown)
168+
169+
# replies come on the control channel
170+
shutdown = reqrep(KernelClient.shutdown, channel='control')
164171

165172

166173
def _stdin_hook_default(self, msg):

jupyter_client/client.py

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ def validate_string_dict(dct):
3535
class KernelClient(ConnectionFileMixin):
3636
"""Communicates with a single kernel on any host via zmq channels.
3737
38-
There are four channels associated with each kernel:
38+
There are five channels associated with each kernel:
3939
4040
* shell: for request/reply calls to the kernel.
4141
* iopub: for the kernel to publish results to frontends.
4242
* hb: for monitoring the kernel's heartbeat.
4343
* stdin: for frontends to reply to raw_input calls in the kernel.
44+
* control: for kernel management calls to the kernel.
4445
4546
The messages that can be sent on these channels are exposed as methods of the
4647
client (KernelClient.execute, complete, history, etc.). These methods only
@@ -58,12 +59,14 @@ def _context_default(self):
5859
iopub_channel_class = Type(ChannelABC)
5960
stdin_channel_class = Type(ChannelABC)
6061
hb_channel_class = Type(HBChannelABC)
62+
control_channel_class = Type(ChannelABC)
6163

6264
# Protected traits
6365
_shell_channel = Any()
6466
_iopub_channel = Any()
6567
_stdin_channel = Any()
6668
_hb_channel = Any()
69+
_control_channel = Any()
6770

6871
# flag for whether execute requests should be allowed to call raw_input:
6972
allow_stdin = True
@@ -84,11 +87,15 @@ def get_stdin_msg(self, *args, **kwargs):
8487
"""Get a message from the stdin channel"""
8588
return self.stdin_channel.get_msg(*args, **kwargs)
8689

90+
def get_control_msg(self, *args, **kwargs):
91+
"""Get a message from the control channel"""
92+
return self.control_channel.get_msg(*args, **kwargs)
93+
8794
#--------------------------------------------------------------------------
8895
# Channel management methods
8996
#--------------------------------------------------------------------------
9097

91-
def start_channels(self, shell=True, iopub=True, stdin=True, hb=True):
98+
def start_channels(self, shell=True, iopub=True, stdin=True, hb=True, control=True):
9299
"""Starts the channels for this kernel.
93100
94101
This will create the channels if they do not exist and then start
@@ -109,6 +116,8 @@ def start_channels(self, shell=True, iopub=True, stdin=True, hb=True):
109116
self.allow_stdin = False
110117
if hb:
111118
self.hb_channel.start()
119+
if control:
120+
self.control_channel.start()
112121

113122
def stop_channels(self):
114123
"""Stops all the running channels for this kernel.
@@ -123,12 +132,15 @@ def stop_channels(self):
123132
self.stdin_channel.stop()
124133
if self.hb_channel.is_alive():
125134
self.hb_channel.stop()
135+
if self.control_channel.is_alive():
136+
self.control_channel.stop()
126137

127138
@property
128139
def channels_running(self):
129140
"""Are any of the channels created and running?"""
130141
return (self.shell_channel.is_alive() or self.iopub_channel.is_alive() or
131-
self.stdin_channel.is_alive() or self.hb_channel.is_alive())
142+
self.stdin_channel.is_alive() or self.hb_channel.is_alive() or
143+
self.control_channel.is_alive())
132144

133145
ioloop = None # Overridden in subclasses that use pyzmq event loop
134146

@@ -179,6 +191,18 @@ def hb_channel(self):
179191
)
180192
return self._hb_channel
181193

194+
@property
195+
def control_channel(self):
196+
"""Get the control channel object for this kernel."""
197+
if self._control_channel is None:
198+
url = self._make_url('control')
199+
self.log.debug("connecting control channel to %s", url)
200+
socket = self.connect_control(identity=self.session.bsession)
201+
self._control_channel = self.control_channel_class(
202+
socket, self.session, self.ioloop
203+
)
204+
return self._control_channel
205+
182206
def is_alive(self):
183207
"""Is the kernel process still running?"""
184208
from .manager import KernelManager
@@ -383,8 +407,24 @@ def _handle_kernel_info_reply(self, msg):
383407
if adapt_version != major_protocol_version:
384408
self.session.adapt_version = adapt_version
385409

410+
def is_complete(self, code):
411+
"""Ask the kernel whether some code is complete and ready to execute."""
412+
msg = self.session.msg('is_complete_request', {'code': code})
413+
self.shell_channel.send(msg)
414+
return msg['header']['msg_id']
415+
416+
def input(self, string):
417+
"""Send a string of raw input to the kernel.
418+
419+
This should only be called in response to the kernel sending an
420+
``input_request`` message on the stdin channel.
421+
"""
422+
content = dict(value=string)
423+
msg = self.session.msg('input_reply', content)
424+
self.stdin_channel.send(msg)
425+
386426
def shutdown(self, restart=False):
387-
"""Request an immediate kernel shutdown.
427+
"""Request an immediate kernel shutdown on the control channel.
388428
389429
Upon receipt of the (empty) reply, client code can safely assume that
390430
the kernel has shut down and it's safe to forcefully terminate it if
@@ -401,24 +441,7 @@ def shutdown(self, restart=False):
401441
# Send quit message to kernel. Once we implement kernel-side setattr,
402442
# this should probably be done that way, but for now this will do.
403443
msg = self.session.msg('shutdown_request', {'restart':restart})
404-
self.shell_channel.send(msg)
405-
return msg['header']['msg_id']
406-
407-
def is_complete(self, code):
408-
"""Ask the kernel whether some code is complete and ready to execute."""
409-
msg = self.session.msg('is_complete_request', {'code': code})
410-
self.shell_channel.send(msg)
444+
self.control_channel.send(msg)
411445
return msg['header']['msg_id']
412446

413-
def input(self, string):
414-
"""Send a string of raw input to the kernel.
415-
416-
This should only be called in response to the kernel sending an
417-
``input_request`` message on the stdin channel.
418-
"""
419-
content = dict(value=string)
420-
msg = self.session.msg('input_reply', content)
421-
self.stdin_channel.send(msg)
422-
423-
424447
KernelClientABC.register(KernelClient)

jupyter_client/clientabc.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,16 @@ def hb_channel_class(self):
4747
def stdin_channel_class(self):
4848
pass
4949

50+
@abc.abstractproperty
51+
def control_channel_class(self):
52+
pass
53+
5054
#--------------------------------------------------------------------------
5155
# Channel management methods
5256
#--------------------------------------------------------------------------
5357

5458
@abc.abstractmethod
55-
def start_channels(self, shell=True, iopub=True, stdin=True, hb=True):
59+
def start_channels(self, shell=True, iopub=True, stdin=True, hb=True, control=True):
5660
pass
5761

5862
@abc.abstractmethod
@@ -78,3 +82,7 @@ def stdin_channel(self):
7882
@abc.abstractproperty
7983
def hb_channel(self):
8084
pass
85+
86+
@abc.abstractproperty
87+
def control_channel(self):
88+
pass

jupyter_client/connect.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ def find_connection_file(filename='kernel-*.json', path=None, profile=None):
226226
def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
227227
"""tunnel connections to a kernel via ssh
228228
229-
This will open four SSH tunnels from localhost on this machine to the
229+
This will open five SSH tunnels from localhost on this machine to the
230230
ports associated with the kernel. They can be either direct
231231
localhost-localhost tunnels, or if an intermediate server is necessary,
232232
the kernel must be listening on a public IP.
@@ -246,8 +246,8 @@ def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
246246
Returns
247247
-------
248248
249-
(shell, iopub, stdin, hb) : ints
250-
The four ports on localhost that have been forwarded to the kernel.
249+
(shell, iopub, stdin, hb, control) : ints
250+
The five ports on localhost that have been forwarded to the kernel.
251251
"""
252252
from .ssh import tunnel
253253
if isinstance(connection_info, string_types):
@@ -257,8 +257,8 @@ def tunnel_to_kernel(connection_info, sshserver, sshkey=None):
257257

258258
cf = connection_info
259259

260-
lports = tunnel.select_random_ports(4)
261-
rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port']
260+
lports = tunnel.select_random_ports(5)
261+
rports = cf['shell_port'], cf['iopub_port'], cf['stdin_port'], cf['hb_port'], cf['control_port']
262262

263263
remote_ip = cf['ip']
264264

0 commit comments

Comments
 (0)