Uploaded Test files
This commit is contained in:
parent
f584ad9d97
commit
2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions
Binary file not shown.
Binary file not shown.
Binary file not shown.
496
venv/Lib/site-packages/notebook/services/kernels/handlers.py
Normal file
496
venv/Lib/site-packages/notebook/services/kernels/handlers.py
Normal file
|
@ -0,0 +1,496 @@
|
|||
"""Tornado handlers for kernels.
|
||||
|
||||
Preliminary documentation at https://github.com/ipython/ipython/wiki/IPEP-16%3A-Notebook-multi-directory-dashboard-and-URL-mapping#kernels-api
|
||||
"""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
from textwrap import dedent
|
||||
|
||||
from tornado import gen, web
|
||||
from tornado.concurrent import Future
|
||||
from tornado.ioloop import IOLoop
|
||||
|
||||
from jupyter_client import protocol_version as client_protocol_version
|
||||
from jupyter_client.jsonutil import date_default
|
||||
from ipython_genutils.py3compat import cast_unicode
|
||||
from notebook.utils import maybe_future, url_path_join, url_escape
|
||||
|
||||
from ...base.handlers import APIHandler
|
||||
from ...base.zmqhandlers import AuthenticatedZMQStreamHandler, deserialize_binary_message
|
||||
|
||||
class MainKernelHandler(APIHandler):
|
||||
|
||||
@web.authenticated
|
||||
@gen.coroutine
|
||||
def get(self):
|
||||
km = self.kernel_manager
|
||||
kernels = yield maybe_future(km.list_kernels())
|
||||
self.finish(json.dumps(kernels, default=date_default))
|
||||
|
||||
@web.authenticated
|
||||
@gen.coroutine
|
||||
def post(self):
|
||||
km = self.kernel_manager
|
||||
model = self.get_json_body()
|
||||
if model is None:
|
||||
model = {
|
||||
'name': km.default_kernel_name
|
||||
}
|
||||
else:
|
||||
model.setdefault('name', km.default_kernel_name)
|
||||
|
||||
kernel_id = yield maybe_future(km.start_kernel(kernel_name=model['name']))
|
||||
model = yield maybe_future(km.kernel_model(kernel_id))
|
||||
location = url_path_join(self.base_url, 'api', 'kernels', url_escape(kernel_id))
|
||||
self.set_header('Location', location)
|
||||
self.set_status(201)
|
||||
self.finish(json.dumps(model, default=date_default))
|
||||
|
||||
|
||||
class KernelHandler(APIHandler):
|
||||
|
||||
@web.authenticated
|
||||
def get(self, kernel_id):
|
||||
km = self.kernel_manager
|
||||
model = km.kernel_model(kernel_id)
|
||||
self.finish(json.dumps(model, default=date_default))
|
||||
|
||||
@web.authenticated
|
||||
@gen.coroutine
|
||||
def delete(self, kernel_id):
|
||||
km = self.kernel_manager
|
||||
yield maybe_future(km.shutdown_kernel(kernel_id))
|
||||
self.set_status(204)
|
||||
self.finish()
|
||||
|
||||
|
||||
class KernelActionHandler(APIHandler):
|
||||
|
||||
@web.authenticated
|
||||
@gen.coroutine
|
||||
def post(self, kernel_id, action):
|
||||
km = self.kernel_manager
|
||||
if action == 'interrupt':
|
||||
yield maybe_future(km.interrupt_kernel(kernel_id))
|
||||
self.set_status(204)
|
||||
if action == 'restart':
|
||||
|
||||
try:
|
||||
yield maybe_future(km.restart_kernel(kernel_id))
|
||||
except Exception as e:
|
||||
self.log.error("Exception restarting kernel", exc_info=True)
|
||||
self.set_status(500)
|
||||
else:
|
||||
model = yield maybe_future(km.kernel_model(kernel_id))
|
||||
self.write(json.dumps(model, default=date_default))
|
||||
self.finish()
|
||||
|
||||
|
||||
class ZMQChannelsHandler(AuthenticatedZMQStreamHandler):
|
||||
'''There is one ZMQChannelsHandler per running kernel and it oversees all
|
||||
the sessions.
|
||||
'''
|
||||
|
||||
# class-level registry of open sessions
|
||||
# allows checking for conflict on session-id,
|
||||
# which is used as a zmq identity and must be unique.
|
||||
_open_sessions = {}
|
||||
|
||||
@property
|
||||
def kernel_info_timeout(self):
|
||||
km_default = self.kernel_manager.kernel_info_timeout
|
||||
return self.settings.get('kernel_info_timeout', km_default)
|
||||
|
||||
@property
|
||||
def iopub_msg_rate_limit(self):
|
||||
return self.settings.get('iopub_msg_rate_limit', 0)
|
||||
|
||||
@property
|
||||
def iopub_data_rate_limit(self):
|
||||
return self.settings.get('iopub_data_rate_limit', 0)
|
||||
|
||||
@property
|
||||
def rate_limit_window(self):
|
||||
return self.settings.get('rate_limit_window', 1.0)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(%s)" % (self.__class__.__name__, getattr(self, 'kernel_id', 'uninitialized'))
|
||||
|
||||
def create_stream(self):
|
||||
km = self.kernel_manager
|
||||
identity = self.session.bsession
|
||||
for channel in ('shell', 'control', 'iopub', 'stdin'):
|
||||
meth = getattr(km, 'connect_' + channel)
|
||||
self.channels[channel] = stream = meth(self.kernel_id, identity=identity)
|
||||
stream.channel = channel
|
||||
|
||||
def request_kernel_info(self):
|
||||
"""send a request for kernel_info"""
|
||||
km = self.kernel_manager
|
||||
kernel = km.get_kernel(self.kernel_id)
|
||||
try:
|
||||
# check for previous request
|
||||
future = kernel._kernel_info_future
|
||||
except AttributeError:
|
||||
self.log.debug("Requesting kernel info from %s", self.kernel_id)
|
||||
# Create a kernel_info channel to query the kernel protocol version.
|
||||
# This channel will be closed after the kernel_info reply is received.
|
||||
if self.kernel_info_channel is None:
|
||||
self.kernel_info_channel = km.connect_shell(self.kernel_id)
|
||||
self.kernel_info_channel.on_recv(self._handle_kernel_info_reply)
|
||||
self.session.send(self.kernel_info_channel, "kernel_info_request")
|
||||
# store the future on the kernel, so only one request is sent
|
||||
kernel._kernel_info_future = self._kernel_info_future
|
||||
else:
|
||||
if not future.done():
|
||||
self.log.debug("Waiting for pending kernel_info request")
|
||||
future.add_done_callback(lambda f: self._finish_kernel_info(f.result()))
|
||||
return self._kernel_info_future
|
||||
|
||||
def _handle_kernel_info_reply(self, msg):
|
||||
"""process the kernel_info_reply
|
||||
|
||||
enabling msg spec adaptation, if necessary
|
||||
"""
|
||||
idents,msg = self.session.feed_identities(msg)
|
||||
try:
|
||||
msg = self.session.deserialize(msg)
|
||||
except:
|
||||
self.log.error("Bad kernel_info reply", exc_info=True)
|
||||
self._kernel_info_future.set_result({})
|
||||
return
|
||||
else:
|
||||
info = msg['content']
|
||||
self.log.debug("Received kernel info: %s", info)
|
||||
if msg['msg_type'] != 'kernel_info_reply' or 'protocol_version' not in info:
|
||||
self.log.error("Kernel info request failed, assuming current %s", info)
|
||||
info = {}
|
||||
self._finish_kernel_info(info)
|
||||
|
||||
# close the kernel_info channel, we don't need it anymore
|
||||
if self.kernel_info_channel:
|
||||
self.kernel_info_channel.close()
|
||||
self.kernel_info_channel = None
|
||||
|
||||
def _finish_kernel_info(self, info):
|
||||
"""Finish handling kernel_info reply
|
||||
|
||||
Set up protocol adaptation, if needed,
|
||||
and signal that connection can continue.
|
||||
"""
|
||||
protocol_version = info.get('protocol_version', client_protocol_version)
|
||||
if protocol_version != client_protocol_version:
|
||||
self.session.adapt_version = int(protocol_version.split('.')[0])
|
||||
self.log.info("Adapting from protocol version {protocol_version} (kernel {kernel_id}) to {client_protocol_version} (client).".format(protocol_version=protocol_version, kernel_id=self.kernel_id, client_protocol_version=client_protocol_version))
|
||||
if not self._kernel_info_future.done():
|
||||
self._kernel_info_future.set_result(info)
|
||||
|
||||
def initialize(self):
|
||||
super(ZMQChannelsHandler, self).initialize()
|
||||
self.zmq_stream = None
|
||||
self.channels = {}
|
||||
self.kernel_id = None
|
||||
self.kernel_info_channel = None
|
||||
self._kernel_info_future = Future()
|
||||
self._close_future = Future()
|
||||
self.session_key = ''
|
||||
|
||||
# Rate limiting code
|
||||
self._iopub_window_msg_count = 0
|
||||
self._iopub_window_byte_count = 0
|
||||
self._iopub_msgs_exceeded = False
|
||||
self._iopub_data_exceeded = False
|
||||
# Queue of (time stamp, byte count)
|
||||
# Allows you to specify that the byte count should be lowered
|
||||
# by a delta amount at some point in the future.
|
||||
self._iopub_window_byte_queue = []
|
||||
|
||||
@gen.coroutine
|
||||
def pre_get(self):
|
||||
# authenticate first
|
||||
super(ZMQChannelsHandler, self).pre_get()
|
||||
# check session collision:
|
||||
yield self._register_session()
|
||||
# then request kernel info, waiting up to a certain time before giving up.
|
||||
# We don't want to wait forever, because browsers don't take it well when
|
||||
# servers never respond to websocket connection requests.
|
||||
kernel = self.kernel_manager.get_kernel(self.kernel_id)
|
||||
self.session.key = kernel.session.key
|
||||
future = self.request_kernel_info()
|
||||
|
||||
def give_up():
|
||||
"""Don't wait forever for the kernel to reply"""
|
||||
if future.done():
|
||||
return
|
||||
self.log.warning("Timeout waiting for kernel_info reply from %s", self.kernel_id)
|
||||
future.set_result({})
|
||||
loop = IOLoop.current()
|
||||
loop.add_timeout(loop.time() + self.kernel_info_timeout, give_up)
|
||||
# actually wait for it
|
||||
yield future
|
||||
|
||||
@gen.coroutine
|
||||
def get(self, kernel_id):
|
||||
self.kernel_id = cast_unicode(kernel_id, 'ascii')
|
||||
yield super(ZMQChannelsHandler, self).get(kernel_id=kernel_id)
|
||||
|
||||
@gen.coroutine
|
||||
def _register_session(self):
|
||||
"""Ensure we aren't creating a duplicate session.
|
||||
|
||||
If a previous identical session is still open, close it to avoid collisions.
|
||||
This is likely due to a client reconnecting from a lost network connection,
|
||||
where the socket on our side has not been cleaned up yet.
|
||||
"""
|
||||
self.session_key = '%s:%s' % (self.kernel_id, self.session.session)
|
||||
stale_handler = self._open_sessions.get(self.session_key)
|
||||
if stale_handler:
|
||||
self.log.warning("Replacing stale connection: %s", self.session_key)
|
||||
yield stale_handler.close()
|
||||
self._open_sessions[self.session_key] = self
|
||||
|
||||
def open(self, kernel_id):
|
||||
super(ZMQChannelsHandler, self).open()
|
||||
km = self.kernel_manager
|
||||
km.notify_connect(kernel_id)
|
||||
|
||||
# on new connections, flush the message buffer
|
||||
buffer_info = km.get_buffer(kernel_id, self.session_key)
|
||||
if buffer_info and buffer_info['session_key'] == self.session_key:
|
||||
self.log.info("Restoring connection for %s", self.session_key)
|
||||
self.channels = buffer_info['channels']
|
||||
replay_buffer = buffer_info['buffer']
|
||||
if replay_buffer:
|
||||
self.log.info("Replaying %s buffered messages", len(replay_buffer))
|
||||
for channel, msg_list in replay_buffer:
|
||||
stream = self.channels[channel]
|
||||
self._on_zmq_reply(stream, msg_list)
|
||||
else:
|
||||
try:
|
||||
self.create_stream()
|
||||
except web.HTTPError as e:
|
||||
self.log.error("Error opening stream: %s", e)
|
||||
# WebSockets don't response to traditional error codes so we
|
||||
# close the connection.
|
||||
for channel, stream in self.channels.items():
|
||||
if not stream.closed():
|
||||
stream.close()
|
||||
self.close()
|
||||
return
|
||||
|
||||
km.add_restart_callback(self.kernel_id, self.on_kernel_restarted)
|
||||
km.add_restart_callback(self.kernel_id, self.on_restart_failed, 'dead')
|
||||
|
||||
for channel, stream in self.channels.items():
|
||||
stream.on_recv_stream(self._on_zmq_reply)
|
||||
|
||||
def on_message(self, msg):
|
||||
if not self.channels:
|
||||
# already closed, ignore the message
|
||||
self.log.debug("Received message on closed websocket %r", msg)
|
||||
return
|
||||
if isinstance(msg, bytes):
|
||||
msg = deserialize_binary_message(msg)
|
||||
else:
|
||||
msg = json.loads(msg)
|
||||
channel = msg.pop('channel', None)
|
||||
if channel is None:
|
||||
self.log.warning("No channel specified, assuming shell: %s", msg)
|
||||
channel = 'shell'
|
||||
if channel not in self.channels:
|
||||
self.log.warning("No such channel: %r", channel)
|
||||
return
|
||||
am = self.kernel_manager.allowed_message_types
|
||||
mt = msg['header']['msg_type']
|
||||
if am and mt not in am:
|
||||
self.log.warning('Received message of type "%s", which is not allowed. Ignoring.' % mt)
|
||||
else:
|
||||
stream = self.channels[channel]
|
||||
self.session.send(stream, msg)
|
||||
|
||||
def _on_zmq_reply(self, stream, msg_list):
|
||||
idents, fed_msg_list = self.session.feed_identities(msg_list)
|
||||
msg = self.session.deserialize(fed_msg_list)
|
||||
parent = msg['parent_header']
|
||||
def write_stderr(error_message):
|
||||
self.log.warning(error_message)
|
||||
msg = self.session.msg("stream",
|
||||
content={"text": error_message + '\n', "name": "stderr"},
|
||||
parent=parent
|
||||
)
|
||||
msg['channel'] = 'iopub'
|
||||
self.write_message(json.dumps(msg, default=date_default))
|
||||
channel = getattr(stream, 'channel', None)
|
||||
msg_type = msg['header']['msg_type']
|
||||
|
||||
if channel == 'iopub' and msg_type == 'status' and msg['content'].get('execution_state') == 'idle':
|
||||
# reset rate limit counter on status=idle,
|
||||
# to avoid 'Run All' hitting limits prematurely.
|
||||
self._iopub_window_byte_queue = []
|
||||
self._iopub_window_msg_count = 0
|
||||
self._iopub_window_byte_count = 0
|
||||
self._iopub_msgs_exceeded = False
|
||||
self._iopub_data_exceeded = False
|
||||
|
||||
if channel == 'iopub' and msg_type not in {'status', 'comm_open', 'execute_input'}:
|
||||
|
||||
# Remove the counts queued for removal.
|
||||
now = IOLoop.current().time()
|
||||
while len(self._iopub_window_byte_queue) > 0:
|
||||
queued = self._iopub_window_byte_queue[0]
|
||||
if (now >= queued[0]):
|
||||
self._iopub_window_byte_count -= queued[1]
|
||||
self._iopub_window_msg_count -= 1
|
||||
del self._iopub_window_byte_queue[0]
|
||||
else:
|
||||
# This part of the queue hasn't be reached yet, so we can
|
||||
# abort the loop.
|
||||
break
|
||||
|
||||
# Increment the bytes and message count
|
||||
self._iopub_window_msg_count += 1
|
||||
if msg_type == 'stream':
|
||||
byte_count = sum([len(x) for x in msg_list])
|
||||
else:
|
||||
byte_count = 0
|
||||
self._iopub_window_byte_count += byte_count
|
||||
|
||||
# Queue a removal of the byte and message count for a time in the
|
||||
# future, when we are no longer interested in it.
|
||||
self._iopub_window_byte_queue.append((now + self.rate_limit_window, byte_count))
|
||||
|
||||
# Check the limits, set the limit flags, and reset the
|
||||
# message and data counts.
|
||||
msg_rate = float(self._iopub_window_msg_count) / self.rate_limit_window
|
||||
data_rate = float(self._iopub_window_byte_count) / self.rate_limit_window
|
||||
|
||||
# Check the msg rate
|
||||
if self.iopub_msg_rate_limit > 0 and msg_rate > self.iopub_msg_rate_limit:
|
||||
if not self._iopub_msgs_exceeded:
|
||||
self._iopub_msgs_exceeded = True
|
||||
write_stderr(dedent("""\
|
||||
IOPub message rate exceeded.
|
||||
The notebook server will temporarily stop sending output
|
||||
to the client in order to avoid crashing it.
|
||||
To change this limit, set the config variable
|
||||
`--NotebookApp.iopub_msg_rate_limit`.
|
||||
|
||||
Current values:
|
||||
NotebookApp.iopub_msg_rate_limit={} (msgs/sec)
|
||||
NotebookApp.rate_limit_window={} (secs)
|
||||
""".format(self.iopub_msg_rate_limit, self.rate_limit_window)))
|
||||
else:
|
||||
# resume once we've got some headroom below the limit
|
||||
if self._iopub_msgs_exceeded and msg_rate < (0.8 * self.iopub_msg_rate_limit):
|
||||
self._iopub_msgs_exceeded = False
|
||||
if not self._iopub_data_exceeded:
|
||||
self.log.warning("iopub messages resumed")
|
||||
|
||||
# Check the data rate
|
||||
if self.iopub_data_rate_limit > 0 and data_rate > self.iopub_data_rate_limit:
|
||||
if not self._iopub_data_exceeded:
|
||||
self._iopub_data_exceeded = True
|
||||
write_stderr(dedent("""\
|
||||
IOPub data rate exceeded.
|
||||
The notebook server will temporarily stop sending output
|
||||
to the client in order to avoid crashing it.
|
||||
To change this limit, set the config variable
|
||||
`--NotebookApp.iopub_data_rate_limit`.
|
||||
|
||||
Current values:
|
||||
NotebookApp.iopub_data_rate_limit={} (bytes/sec)
|
||||
NotebookApp.rate_limit_window={} (secs)
|
||||
""".format(self.iopub_data_rate_limit, self.rate_limit_window)))
|
||||
else:
|
||||
# resume once we've got some headroom below the limit
|
||||
if self._iopub_data_exceeded and data_rate < (0.8 * self.iopub_data_rate_limit):
|
||||
self._iopub_data_exceeded = False
|
||||
if not self._iopub_msgs_exceeded:
|
||||
self.log.warning("iopub messages resumed")
|
||||
|
||||
# If either of the limit flags are set, do not send the message.
|
||||
if self._iopub_msgs_exceeded or self._iopub_data_exceeded:
|
||||
# we didn't send it, remove the current message from the calculus
|
||||
self._iopub_window_msg_count -= 1
|
||||
self._iopub_window_byte_count -= byte_count
|
||||
self._iopub_window_byte_queue.pop(-1)
|
||||
return
|
||||
super(ZMQChannelsHandler, self)._on_zmq_reply(stream, msg)
|
||||
|
||||
def close(self):
|
||||
super(ZMQChannelsHandler, self).close()
|
||||
return self._close_future
|
||||
|
||||
def on_close(self):
|
||||
self.log.debug("Websocket closed %s", self.session_key)
|
||||
# unregister myself as an open session (only if it's really me)
|
||||
if self._open_sessions.get(self.session_key) is self:
|
||||
self._open_sessions.pop(self.session_key)
|
||||
|
||||
km = self.kernel_manager
|
||||
if self.kernel_id in km:
|
||||
km.notify_disconnect(self.kernel_id)
|
||||
km.remove_restart_callback(
|
||||
self.kernel_id, self.on_kernel_restarted,
|
||||
)
|
||||
km.remove_restart_callback(
|
||||
self.kernel_id, self.on_restart_failed, 'dead',
|
||||
)
|
||||
|
||||
# start buffering instead of closing if this was the last connection
|
||||
if km._kernel_connections[self.kernel_id] == 0:
|
||||
km.start_buffering(self.kernel_id, self.session_key, self.channels)
|
||||
self._close_future.set_result(None)
|
||||
return
|
||||
|
||||
# This method can be called twice, once by self.kernel_died and once
|
||||
# from the WebSocket close event. If the WebSocket connection is
|
||||
# closed before the ZMQ streams are setup, they could be None.
|
||||
for channel, stream in self.channels.items():
|
||||
if stream is not None and not stream.closed():
|
||||
stream.on_recv(None)
|
||||
stream.close()
|
||||
|
||||
self.channels = {}
|
||||
self._close_future.set_result(None)
|
||||
|
||||
def _send_status_message(self, status):
|
||||
iopub = self.channels.get('iopub', None)
|
||||
if iopub and not iopub.closed():
|
||||
# flush IOPub before sending a restarting/dead status message
|
||||
# ensures proper ordering on the IOPub channel
|
||||
# that all messages from the stopped kernel have been delivered
|
||||
iopub.flush()
|
||||
msg = self.session.msg("status",
|
||||
{'execution_state': status}
|
||||
)
|
||||
msg['channel'] = 'iopub'
|
||||
self.write_message(json.dumps(msg, default=date_default))
|
||||
|
||||
def on_kernel_restarted(self):
|
||||
logging.warn("kernel %s restarted", self.kernel_id)
|
||||
self._send_status_message('restarting')
|
||||
|
||||
def on_restart_failed(self):
|
||||
logging.error("kernel %s restarted failed!", self.kernel_id)
|
||||
self._send_status_message('dead')
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
# URL to handler mappings
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
_kernel_id_regex = r"(?P<kernel_id>\w+-\w+-\w+-\w+-\w+)"
|
||||
_kernel_action_regex = r"(?P<action>restart|interrupt)"
|
||||
|
||||
default_handlers = [
|
||||
(r"/api/kernels", MainKernelHandler),
|
||||
(r"/api/kernels/%s" % _kernel_id_regex, KernelHandler),
|
||||
(r"/api/kernels/%s/%s" % (_kernel_id_regex, _kernel_action_regex), KernelActionHandler),
|
||||
(r"/api/kernels/%s/channels" % _kernel_id_regex, ZMQChannelsHandler),
|
||||
]
|
|
@ -0,0 +1,509 @@
|
|||
"""A MultiKernelManager for use in the notebook webserver
|
||||
- raises HTTPErrors
|
||||
- creates REST API models
|
||||
"""
|
||||
|
||||
# Copyright (c) Jupyter Development Team.
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
import os
|
||||
|
||||
from tornado import web
|
||||
from tornado.concurrent import Future
|
||||
from tornado.ioloop import IOLoop, PeriodicCallback
|
||||
|
||||
from jupyter_client.session import Session
|
||||
from jupyter_client.multikernelmanager import MultiKernelManager
|
||||
from traitlets import (Any, Bool, Dict, List, Unicode, TraitError, Integer,
|
||||
Float, Instance, default, validate
|
||||
)
|
||||
|
||||
from notebook.utils import maybe_future, to_os_path, exists
|
||||
from notebook._tz import utcnow, isoformat
|
||||
from ipython_genutils.py3compat import getcwd
|
||||
|
||||
from notebook.prometheus.metrics import KERNEL_CURRENTLY_RUNNING_TOTAL
|
||||
|
||||
# Since use of AsyncMultiKernelManager is optional at the moment, don't require appropriate jupyter_client.
|
||||
# This will be confirmed at runtime in notebookapp. The following block can be removed once the jupyter_client's
|
||||
# floor has been updated.
|
||||
try:
|
||||
from jupyter_client.multikernelmanager import AsyncMultiKernelManager
|
||||
except ImportError:
|
||||
class AsyncMultiKernelManager(object):
|
||||
"""Empty class to satisfy unused reference by AsyncMappingKernelManager."""
|
||||
def __init__(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
class MappingKernelManager(MultiKernelManager):
|
||||
"""A KernelManager that handles notebook mapping and HTTP error handling"""
|
||||
|
||||
@default('kernel_manager_class')
|
||||
def _default_kernel_manager_class(self):
|
||||
return "jupyter_client.ioloop.IOLoopKernelManager"
|
||||
|
||||
kernel_argv = List(Unicode())
|
||||
|
||||
root_dir = Unicode(config=True)
|
||||
|
||||
_kernel_connections = Dict()
|
||||
|
||||
_culler_callback = None
|
||||
|
||||
_initialized_culler = False
|
||||
|
||||
@default('root_dir')
|
||||
def _default_root_dir(self):
|
||||
try:
|
||||
return self.parent.notebook_dir
|
||||
except AttributeError:
|
||||
return getcwd()
|
||||
|
||||
@validate('root_dir')
|
||||
def _update_root_dir(self, proposal):
|
||||
"""Do a bit of validation of the root dir."""
|
||||
value = proposal['value']
|
||||
if not os.path.isabs(value):
|
||||
# If we receive a non-absolute path, make it absolute.
|
||||
value = os.path.abspath(value)
|
||||
if not exists(value) or not os.path.isdir(value):
|
||||
raise TraitError("kernel root dir %r is not a directory" % value)
|
||||
return value
|
||||
|
||||
cull_idle_timeout = Integer(0, config=True,
|
||||
help="""Timeout (in seconds) after which a kernel is considered idle and ready to be culled.
|
||||
Values of 0 or lower disable culling. Very short timeouts may result in kernels being culled
|
||||
for users with poor network connections."""
|
||||
)
|
||||
|
||||
cull_interval_default = 300 # 5 minutes
|
||||
cull_interval = Integer(cull_interval_default, config=True,
|
||||
help="""The interval (in seconds) on which to check for idle kernels exceeding the cull timeout value."""
|
||||
)
|
||||
|
||||
cull_connected = Bool(False, config=True,
|
||||
help="""Whether to consider culling kernels which have one or more connections.
|
||||
Only effective if cull_idle_timeout > 0."""
|
||||
)
|
||||
|
||||
cull_busy = Bool(False, config=True,
|
||||
help="""Whether to consider culling kernels which are busy.
|
||||
Only effective if cull_idle_timeout > 0."""
|
||||
)
|
||||
|
||||
buffer_offline_messages = Bool(True, config=True,
|
||||
help="""Whether messages from kernels whose frontends have disconnected should be buffered in-memory.
|
||||
When True (default), messages are buffered and replayed on reconnect,
|
||||
avoiding lost messages due to interrupted connectivity.
|
||||
Disable if long-running kernels will produce too much output while
|
||||
no frontends are connected.
|
||||
"""
|
||||
)
|
||||
|
||||
kernel_info_timeout = Float(60, config=True,
|
||||
help="""Timeout for giving up on a kernel (in seconds).
|
||||
On starting and restarting kernels, we check whether the
|
||||
kernel is running and responsive by sending kernel_info_requests.
|
||||
This sets the timeout in seconds for how long the kernel can take
|
||||
before being presumed dead.
|
||||
This affects the MappingKernelManager (which handles kernel restarts)
|
||||
and the ZMQChannelsHandler (which handles the startup).
|
||||
"""
|
||||
)
|
||||
|
||||
_kernel_buffers = Any()
|
||||
@default('_kernel_buffers')
|
||||
def _default_kernel_buffers(self):
|
||||
return defaultdict(lambda: {'buffer': [], 'session_key': '', 'channels': {}})
|
||||
|
||||
last_kernel_activity = Instance(datetime,
|
||||
help="The last activity on any kernel, including shutting down a kernel")
|
||||
|
||||
allowed_message_types = List(trait=Unicode(), config=True,
|
||||
help="""White list of allowed kernel message types.
|
||||
When the list is empty, all message types are allowed.
|
||||
"""
|
||||
)
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
# Methods for managing kernels and sessions
|
||||
#-------------------------------------------------------------------------
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# Pin the superclass to better control the MRO. This is needed by
|
||||
# AsyncMappingKernelManager so that it can give priority to methods
|
||||
# on AsyncMultiKernelManager over this superclass.
|
||||
self.pinned_superclass = MultiKernelManager
|
||||
self.pinned_superclass.__init__(self, **kwargs)
|
||||
self.last_kernel_activity = utcnow()
|
||||
|
||||
def _handle_kernel_died(self, kernel_id):
|
||||
"""notice that a kernel died"""
|
||||
self.log.warning("Kernel %s died, removing from map.", kernel_id)
|
||||
self.remove_kernel(kernel_id)
|
||||
|
||||
def cwd_for_path(self, path):
|
||||
"""Turn API path into absolute OS path."""
|
||||
os_path = to_os_path(path, self.root_dir)
|
||||
# in the case of notebooks and kernels not being on the same filesystem,
|
||||
# walk up to root_dir if the paths don't exist
|
||||
while not os.path.isdir(os_path) and os_path != self.root_dir:
|
||||
os_path = os.path.dirname(os_path)
|
||||
return os_path
|
||||
|
||||
async def start_kernel(self, kernel_id=None, path=None, **kwargs):
|
||||
"""Start a kernel for a session and return its kernel_id.
|
||||
Parameters
|
||||
----------
|
||||
kernel_id : uuid
|
||||
The uuid to associate the new kernel with. If this
|
||||
is not None, this kernel will be persistent whenever it is
|
||||
requested.
|
||||
path : API path
|
||||
The API path (unicode, '/' delimited) for the cwd.
|
||||
Will be transformed to an OS path relative to root_dir.
|
||||
kernel_name : str
|
||||
The name identifying which kernel spec to launch. This is ignored if
|
||||
an existing kernel is returned, but it may be checked in the future.
|
||||
"""
|
||||
if kernel_id is None:
|
||||
if path is not None:
|
||||
kwargs['cwd'] = self.cwd_for_path(path)
|
||||
kernel_id = await maybe_future(self.pinned_superclass.start_kernel(self, **kwargs))
|
||||
self._kernel_connections[kernel_id] = 0
|
||||
self.start_watching_activity(kernel_id)
|
||||
self.log.info("Kernel started: %s, name: %s" % (kernel_id, self._kernels[kernel_id].kernel_name))
|
||||
self.log.debug("Kernel args: %r" % kwargs)
|
||||
# register callback for failed auto-restart
|
||||
self.add_restart_callback(kernel_id,
|
||||
lambda : self._handle_kernel_died(kernel_id),
|
||||
'dead',
|
||||
)
|
||||
|
||||
# Increase the metric of number of kernels running
|
||||
# for the relevant kernel type by 1
|
||||
KERNEL_CURRENTLY_RUNNING_TOTAL.labels(
|
||||
type=self._kernels[kernel_id].kernel_name
|
||||
).inc()
|
||||
|
||||
else:
|
||||
self._check_kernel_id(kernel_id)
|
||||
self.log.info("Using existing kernel: %s" % kernel_id)
|
||||
|
||||
# Initialize culling if not already
|
||||
if not self._initialized_culler:
|
||||
self.initialize_culler()
|
||||
|
||||
return kernel_id
|
||||
|
||||
def start_buffering(self, kernel_id, session_key, channels):
|
||||
"""Start buffering messages for a kernel
|
||||
Parameters
|
||||
----------
|
||||
kernel_id : str
|
||||
The id of the kernel to start buffering.
|
||||
session_key: str
|
||||
The session_key, if any, that should get the buffer.
|
||||
If the session_key matches the current buffered session_key,
|
||||
the buffer will be returned.
|
||||
channels: dict({'channel': ZMQStream})
|
||||
The zmq channels whose messages should be buffered.
|
||||
"""
|
||||
|
||||
if not self.buffer_offline_messages:
|
||||
for channel, stream in channels.items():
|
||||
stream.close()
|
||||
return
|
||||
|
||||
self.log.info("Starting buffering for %s", session_key)
|
||||
self._check_kernel_id(kernel_id)
|
||||
# clear previous buffering state
|
||||
self.stop_buffering(kernel_id)
|
||||
buffer_info = self._kernel_buffers[kernel_id]
|
||||
# record the session key because only one session can buffer
|
||||
buffer_info['session_key'] = session_key
|
||||
# TODO: the buffer should likely be a memory bounded queue, we're starting with a list to keep it simple
|
||||
buffer_info['buffer'] = []
|
||||
buffer_info['channels'] = channels
|
||||
|
||||
# forward any future messages to the internal buffer
|
||||
def buffer_msg(channel, msg_parts):
|
||||
self.log.debug("Buffering msg on %s:%s", kernel_id, channel)
|
||||
buffer_info['buffer'].append((channel, msg_parts))
|
||||
|
||||
for channel, stream in channels.items():
|
||||
stream.on_recv(partial(buffer_msg, channel))
|
||||
|
||||
def get_buffer(self, kernel_id, session_key):
|
||||
"""Get the buffer for a given kernel
|
||||
Parameters
|
||||
----------
|
||||
kernel_id : str
|
||||
The id of the kernel to stop buffering.
|
||||
session_key: str, optional
|
||||
The session_key, if any, that should get the buffer.
|
||||
If the session_key matches the current buffered session_key,
|
||||
the buffer will be returned.
|
||||
"""
|
||||
self.log.debug("Getting buffer for %s", kernel_id)
|
||||
if kernel_id not in self._kernel_buffers:
|
||||
return
|
||||
|
||||
buffer_info = self._kernel_buffers[kernel_id]
|
||||
if buffer_info['session_key'] == session_key:
|
||||
# remove buffer
|
||||
self._kernel_buffers.pop(kernel_id)
|
||||
# only return buffer_info if it's a match
|
||||
return buffer_info
|
||||
else:
|
||||
self.stop_buffering(kernel_id)
|
||||
|
||||
def stop_buffering(self, kernel_id):
|
||||
"""Stop buffering kernel messages
|
||||
Parameters
|
||||
----------
|
||||
kernel_id : str
|
||||
The id of the kernel to stop buffering.
|
||||
"""
|
||||
self.log.debug("Clearing buffer for %s", kernel_id)
|
||||
self._check_kernel_id(kernel_id)
|
||||
|
||||
if kernel_id not in self._kernel_buffers:
|
||||
return
|
||||
buffer_info = self._kernel_buffers.pop(kernel_id)
|
||||
# close buffering streams
|
||||
for stream in buffer_info['channels'].values():
|
||||
if not stream.closed():
|
||||
stream.on_recv(None)
|
||||
stream.close()
|
||||
|
||||
msg_buffer = buffer_info['buffer']
|
||||
if msg_buffer:
|
||||
self.log.info("Discarding %s buffered messages for %s",
|
||||
len(msg_buffer), buffer_info['session_key'])
|
||||
|
||||
def shutdown_kernel(self, kernel_id, now=False, restart=False):
|
||||
"""Shutdown a kernel by kernel_id"""
|
||||
self._check_kernel_id(kernel_id)
|
||||
kernel = self._kernels[kernel_id]
|
||||
if kernel._activity_stream:
|
||||
kernel._activity_stream.close()
|
||||
kernel._activity_stream = None
|
||||
self.stop_buffering(kernel_id)
|
||||
self._kernel_connections.pop(kernel_id, None)
|
||||
|
||||
# Decrease the metric of number of kernels
|
||||
# running for the relevant kernel type by 1
|
||||
KERNEL_CURRENTLY_RUNNING_TOTAL.labels(
|
||||
type=self._kernels[kernel_id].kernel_name
|
||||
).dec()
|
||||
|
||||
return self.pinned_superclass.shutdown_kernel(self, kernel_id, now=now, restart=restart)
|
||||
|
||||
async def restart_kernel(self, kernel_id, now=False):
|
||||
"""Restart a kernel by kernel_id"""
|
||||
self._check_kernel_id(kernel_id)
|
||||
await maybe_future(self.pinned_superclass.restart_kernel(self, kernel_id, now=now))
|
||||
kernel = self.get_kernel(kernel_id)
|
||||
# return a Future that will resolve when the kernel has successfully restarted
|
||||
channel = kernel.connect_shell()
|
||||
future = Future()
|
||||
|
||||
def finish():
|
||||
"""Common cleanup when restart finishes/fails for any reason."""
|
||||
if not channel.closed():
|
||||
channel.close()
|
||||
loop.remove_timeout(timeout)
|
||||
kernel.remove_restart_callback(on_restart_failed, 'dead')
|
||||
|
||||
def on_reply(msg):
|
||||
self.log.debug("Kernel info reply received: %s", kernel_id)
|
||||
finish()
|
||||
if not future.done():
|
||||
future.set_result(msg)
|
||||
|
||||
def on_timeout():
|
||||
self.log.warning("Timeout waiting for kernel_info_reply: %s", kernel_id)
|
||||
finish()
|
||||
if not future.done():
|
||||
future.set_exception(TimeoutError("Timeout waiting for restart"))
|
||||
|
||||
def on_restart_failed():
|
||||
self.log.warning("Restarting kernel failed: %s", kernel_id)
|
||||
finish()
|
||||
if not future.done():
|
||||
future.set_exception(RuntimeError("Restart failed"))
|
||||
|
||||
kernel.add_restart_callback(on_restart_failed, 'dead')
|
||||
kernel.session.send(channel, "kernel_info_request")
|
||||
channel.on_recv(on_reply)
|
||||
loop = IOLoop.current()
|
||||
timeout = loop.add_timeout(loop.time() + self.kernel_info_timeout, on_timeout)
|
||||
return future
|
||||
|
||||
def notify_connect(self, kernel_id):
|
||||
"""Notice a new connection to a kernel"""
|
||||
if kernel_id in self._kernel_connections:
|
||||
self._kernel_connections[kernel_id] += 1
|
||||
|
||||
def notify_disconnect(self, kernel_id):
|
||||
"""Notice a disconnection from a kernel"""
|
||||
if kernel_id in self._kernel_connections:
|
||||
self._kernel_connections[kernel_id] -= 1
|
||||
|
||||
def kernel_model(self, kernel_id):
|
||||
"""Return a JSON-safe dict representing a kernel
|
||||
For use in representing kernels in the JSON APIs.
|
||||
"""
|
||||
self._check_kernel_id(kernel_id)
|
||||
kernel = self._kernels[kernel_id]
|
||||
|
||||
model = {
|
||||
"id": kernel_id,
|
||||
"name": kernel.kernel_name,
|
||||
"last_activity": isoformat(kernel.last_activity),
|
||||
"execution_state": kernel.execution_state,
|
||||
"connections": self._kernel_connections[kernel_id],
|
||||
}
|
||||
return model
|
||||
|
||||
def list_kernels(self):
|
||||
"""Returns a list of kernel_id's of kernels running."""
|
||||
kernels = []
|
||||
kernel_ids = self.pinned_superclass.list_kernel_ids(self)
|
||||
for kernel_id in kernel_ids:
|
||||
model = self.kernel_model(kernel_id)
|
||||
kernels.append(model)
|
||||
return kernels
|
||||
|
||||
# override _check_kernel_id to raise 404 instead of KeyError
|
||||
def _check_kernel_id(self, kernel_id):
|
||||
"""Check a that a kernel_id exists and raise 404 if not."""
|
||||
if kernel_id not in self:
|
||||
raise web.HTTPError(404, u'Kernel does not exist: %s' % kernel_id)
|
||||
|
||||
# monitoring activity:
|
||||
|
||||
def start_watching_activity(self, kernel_id):
|
||||
"""Start watching IOPub messages on a kernel for activity.
|
||||
- update last_activity on every message
|
||||
- record execution_state from status messages
|
||||
"""
|
||||
kernel = self._kernels[kernel_id]
|
||||
# add busy/activity markers:
|
||||
kernel.execution_state = 'starting'
|
||||
kernel.last_activity = utcnow()
|
||||
kernel._activity_stream = kernel.connect_iopub()
|
||||
session = Session(
|
||||
config=kernel.session.config,
|
||||
key=kernel.session.key,
|
||||
)
|
||||
|
||||
def record_activity(msg_list):
|
||||
"""Record an IOPub message arriving from a kernel"""
|
||||
self.last_kernel_activity = kernel.last_activity = utcnow()
|
||||
|
||||
idents, fed_msg_list = session.feed_identities(msg_list)
|
||||
msg = session.deserialize(fed_msg_list)
|
||||
|
||||
msg_type = msg['header']['msg_type']
|
||||
if msg_type == 'status':
|
||||
kernel.execution_state = msg['content']['execution_state']
|
||||
self.log.debug("activity on %s: %s (%s)", kernel_id, msg_type, kernel.execution_state)
|
||||
else:
|
||||
self.log.debug("activity on %s: %s", kernel_id, msg_type)
|
||||
|
||||
kernel._activity_stream.on_recv(record_activity)
|
||||
|
||||
def initialize_culler(self):
|
||||
"""Start idle culler if 'cull_idle_timeout' is greater than zero.
|
||||
Regardless of that value, set flag that we've been here.
|
||||
"""
|
||||
if not self._initialized_culler and self.cull_idle_timeout > 0:
|
||||
if self._culler_callback is None:
|
||||
loop = IOLoop.current()
|
||||
if self.cull_interval <= 0: # handle case where user set invalid value
|
||||
self.log.warning("Invalid value for 'cull_interval' detected (%s) - using default value (%s).",
|
||||
self.cull_interval, self.cull_interval_default)
|
||||
self.cull_interval = self.cull_interval_default
|
||||
self._culler_callback = PeriodicCallback(
|
||||
self.cull_kernels, 1000*self.cull_interval)
|
||||
self.log.info("Culling kernels with idle durations > %s seconds at %s second intervals ...",
|
||||
self.cull_idle_timeout, self.cull_interval)
|
||||
if self.cull_busy:
|
||||
self.log.info("Culling kernels even if busy")
|
||||
if self.cull_connected:
|
||||
self.log.info("Culling kernels even with connected clients")
|
||||
self._culler_callback.start()
|
||||
|
||||
self._initialized_culler = True
|
||||
|
||||
async def cull_kernels(self):
|
||||
self.log.debug("Polling every %s seconds for kernels idle > %s seconds...",
|
||||
self.cull_interval, self.cull_idle_timeout)
|
||||
"""Create a separate list of kernels to avoid conflicting updates while iterating"""
|
||||
for kernel_id in list(self._kernels):
|
||||
try:
|
||||
await self.cull_kernel_if_idle(kernel_id)
|
||||
except Exception as e:
|
||||
self.log.exception("The following exception was encountered while checking the "
|
||||
"idle duration of kernel {}: {}".format(kernel_id, e))
|
||||
|
||||
async def cull_kernel_if_idle(self, kernel_id):
|
||||
try:
|
||||
kernel = self._kernels[kernel_id]
|
||||
except KeyError:
|
||||
return # KeyErrors are somewhat expected since the kernel can be shutdown as the culling check is made.
|
||||
|
||||
if hasattr(kernel, 'last_activity'): # last_activity is monkey-patched, so ensure that has occurred
|
||||
self.log.debug("kernel_id=%s, kernel_name=%s, last_activity=%s",
|
||||
kernel_id, kernel.kernel_name, kernel.last_activity)
|
||||
dt_now = utcnow()
|
||||
dt_idle = dt_now - kernel.last_activity
|
||||
# Compute idle properties
|
||||
is_idle_time = dt_idle > timedelta(seconds=self.cull_idle_timeout)
|
||||
is_idle_execute = self.cull_busy or (kernel.execution_state != 'busy')
|
||||
connections = self._kernel_connections.get(kernel_id, 0)
|
||||
is_idle_connected = self.cull_connected or not connections
|
||||
# Cull the kernel if all three criteria are met
|
||||
if (is_idle_time and is_idle_execute and is_idle_connected):
|
||||
idle_duration = int(dt_idle.total_seconds())
|
||||
self.log.warning("Culling '%s' kernel '%s' (%s) with %d connections due to %s seconds of inactivity.",
|
||||
kernel.execution_state, kernel.kernel_name, kernel_id, connections, idle_duration)
|
||||
await maybe_future(self.shutdown_kernel(kernel_id))
|
||||
|
||||
|
||||
# AsyncMappingKernelManager inherits as much as possible from MappingKernelManager, overriding
|
||||
# only what is different.
|
||||
class AsyncMappingKernelManager(MappingKernelManager, AsyncMultiKernelManager):
|
||||
@default('kernel_manager_class')
|
||||
def _default_kernel_manager_class(self):
|
||||
return "jupyter_client.ioloop.AsyncIOLoopKernelManager"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# Pin the superclass to better control the MRO.
|
||||
self.pinned_superclass = AsyncMultiKernelManager
|
||||
self.pinned_superclass.__init__(self, **kwargs)
|
||||
self.last_kernel_activity = utcnow()
|
||||
|
||||
async def shutdown_kernel(self, kernel_id, now=False, restart=False):
|
||||
"""Shutdown a kernel by kernel_id"""
|
||||
self._check_kernel_id(kernel_id)
|
||||
kernel = self._kernels[kernel_id]
|
||||
if kernel._activity_stream:
|
||||
kernel._activity_stream.close()
|
||||
kernel._activity_stream = None
|
||||
self.stop_buffering(kernel_id)
|
||||
self._kernel_connections.pop(kernel_id, None)
|
||||
|
||||
# Decrease the metric of number of kernels
|
||||
# running for the relevant kernel type by 1
|
||||
KERNEL_CURRENTLY_RUNNING_TOTAL.labels(
|
||||
type=self._kernels[kernel_id].kernel_name
|
||||
).dec()
|
||||
|
||||
return await self.pinned_superclass.shutdown_kernel(self, kernel_id, now=now, restart=restart)
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,283 @@
|
|||
"""Test the kernels service API."""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import time
|
||||
|
||||
from requests import HTTPError
|
||||
from traitlets.config import Config
|
||||
|
||||
from tornado.httpclient import HTTPRequest
|
||||
from tornado.ioloop import IOLoop
|
||||
from tornado.websocket import websocket_connect
|
||||
from unittest import SkipTest
|
||||
|
||||
from jupyter_client.kernelspec import NATIVE_KERNEL_NAME
|
||||
|
||||
from notebook.utils import url_path_join
|
||||
from notebook.tests.launchnotebook import NotebookTestBase, assert_http_error
|
||||
|
||||
try:
|
||||
from jupyter_client import AsyncMultiKernelManager
|
||||
async_testing_enabled = True
|
||||
except ImportError:
|
||||
async_testing_enabled = False
|
||||
|
||||
|
||||
class KernelAPI(object):
|
||||
"""Wrapper for kernel REST API requests"""
|
||||
def __init__(self, request, base_url, headers):
|
||||
self.request = request
|
||||
self.base_url = base_url
|
||||
self.headers = headers
|
||||
|
||||
def _req(self, verb, path, body=None):
|
||||
response = self.request(verb,
|
||||
url_path_join('api/kernels', path), data=body)
|
||||
|
||||
if 400 <= response.status_code < 600:
|
||||
try:
|
||||
response.reason = response.json()['message']
|
||||
except:
|
||||
pass
|
||||
response.raise_for_status()
|
||||
|
||||
return response
|
||||
|
||||
def list(self):
|
||||
return self._req('GET', '')
|
||||
|
||||
def get(self, id):
|
||||
return self._req('GET', id)
|
||||
|
||||
def start(self, name=NATIVE_KERNEL_NAME):
|
||||
body = json.dumps({'name': name})
|
||||
return self._req('POST', '', body)
|
||||
|
||||
def shutdown(self, id):
|
||||
return self._req('DELETE', id)
|
||||
|
||||
def interrupt(self, id):
|
||||
return self._req('POST', url_path_join(id, 'interrupt'))
|
||||
|
||||
def restart(self, id):
|
||||
return self._req('POST', url_path_join(id, 'restart'))
|
||||
|
||||
def websocket(self, id):
|
||||
loop = IOLoop()
|
||||
loop.make_current()
|
||||
req = HTTPRequest(
|
||||
url_path_join(self.base_url.replace('http', 'ws', 1), 'api/kernels', id, 'channels'),
|
||||
headers=self.headers,
|
||||
)
|
||||
f = websocket_connect(req)
|
||||
return loop.run_sync(lambda : f)
|
||||
|
||||
|
||||
class KernelAPITest(NotebookTestBase):
|
||||
"""Test the kernels web service API"""
|
||||
def setUp(self):
|
||||
self.kern_api = KernelAPI(self.request,
|
||||
base_url=self.base_url(),
|
||||
headers=self.auth_headers(),
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
for k in self.kern_api.list().json():
|
||||
self.kern_api.shutdown(k['id'])
|
||||
|
||||
def test_no_kernels(self):
|
||||
"""Make sure there are no kernels running at the start"""
|
||||
kernels = self.kern_api.list().json()
|
||||
self.assertEqual(kernels, [])
|
||||
|
||||
def test_default_kernel(self):
|
||||
# POST request
|
||||
r = self.kern_api._req('POST', '')
|
||||
kern1 = r.json()
|
||||
self.assertEqual(r.headers['location'], url_path_join(self.url_prefix, 'api/kernels', kern1['id']))
|
||||
self.assertEqual(r.status_code, 201)
|
||||
self.assertIsInstance(kern1, dict)
|
||||
|
||||
report_uri = url_path_join(self.url_prefix, 'api/security/csp-report')
|
||||
expected_csp = '; '.join([
|
||||
"frame-ancestors 'self'",
|
||||
'report-uri ' + report_uri,
|
||||
"default-src 'none'"
|
||||
])
|
||||
self.assertEqual(r.headers['Content-Security-Policy'], expected_csp)
|
||||
|
||||
def test_main_kernel_handler(self):
|
||||
# POST request
|
||||
r = self.kern_api.start()
|
||||
kern1 = r.json()
|
||||
self.assertEqual(r.headers['location'], url_path_join(self.url_prefix, 'api/kernels', kern1['id']))
|
||||
self.assertEqual(r.status_code, 201)
|
||||
self.assertIsInstance(kern1, dict)
|
||||
|
||||
report_uri = url_path_join(self.url_prefix, 'api/security/csp-report')
|
||||
expected_csp = '; '.join([
|
||||
"frame-ancestors 'self'",
|
||||
'report-uri ' + report_uri,
|
||||
"default-src 'none'"
|
||||
])
|
||||
self.assertEqual(r.headers['Content-Security-Policy'], expected_csp)
|
||||
|
||||
# GET request
|
||||
r = self.kern_api.list()
|
||||
self.assertEqual(r.status_code, 200)
|
||||
assert isinstance(r.json(), list)
|
||||
self.assertEqual(r.json()[0]['id'], kern1['id'])
|
||||
self.assertEqual(r.json()[0]['name'], kern1['name'])
|
||||
|
||||
# create another kernel and check that they both are added to the
|
||||
# list of kernels from a GET request
|
||||
kern2 = self.kern_api.start().json()
|
||||
assert isinstance(kern2, dict)
|
||||
r = self.kern_api.list()
|
||||
kernels = r.json()
|
||||
self.assertEqual(r.status_code, 200)
|
||||
assert isinstance(kernels, list)
|
||||
self.assertEqual(len(kernels), 2)
|
||||
|
||||
# Interrupt a kernel
|
||||
r = self.kern_api.interrupt(kern2['id'])
|
||||
self.assertEqual(r.status_code, 204)
|
||||
|
||||
# Restart a kernel
|
||||
r = self.kern_api.restart(kern2['id'])
|
||||
rekern = r.json()
|
||||
self.assertEqual(rekern['id'], kern2['id'])
|
||||
self.assertEqual(rekern['name'], kern2['name'])
|
||||
|
||||
def test_kernel_handler(self):
|
||||
# GET kernel with given id
|
||||
kid = self.kern_api.start().json()['id']
|
||||
r = self.kern_api.get(kid)
|
||||
kern1 = r.json()
|
||||
self.assertEqual(r.status_code, 200)
|
||||
assert isinstance(kern1, dict)
|
||||
self.assertIn('id', kern1)
|
||||
self.assertEqual(kern1['id'], kid)
|
||||
|
||||
# Request a bad kernel id and check that a JSON
|
||||
# message is returned!
|
||||
bad_id = '111-111-111-111-111'
|
||||
with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
|
||||
self.kern_api.get(bad_id)
|
||||
|
||||
# DELETE kernel with id
|
||||
r = self.kern_api.shutdown(kid)
|
||||
self.assertEqual(r.status_code, 204)
|
||||
kernels = self.kern_api.list().json()
|
||||
self.assertEqual(kernels, [])
|
||||
|
||||
# Request to delete a non-existent kernel id
|
||||
bad_id = '111-111-111-111-111'
|
||||
with assert_http_error(404, 'Kernel does not exist: ' + bad_id):
|
||||
self.kern_api.shutdown(bad_id)
|
||||
|
||||
def test_connections(self):
|
||||
kid = self.kern_api.start().json()['id']
|
||||
model = self.kern_api.get(kid).json()
|
||||
self.assertEqual(model['connections'], 0)
|
||||
|
||||
ws = self.kern_api.websocket(kid)
|
||||
model = self.kern_api.get(kid).json()
|
||||
self.assertEqual(model['connections'], 1)
|
||||
ws.close()
|
||||
# give it some time to close on the other side:
|
||||
for i in range(10):
|
||||
model = self.kern_api.get(kid).json()
|
||||
if model['connections'] > 0:
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
break
|
||||
model = self.kern_api.get(kid).json()
|
||||
self.assertEqual(model['connections'], 0)
|
||||
|
||||
|
||||
class AsyncKernelAPITest(KernelAPITest):
|
||||
"""Test the kernels web service API using the AsyncMappingKernelManager"""
|
||||
|
||||
@classmethod
|
||||
def setup_class(cls):
|
||||
if not async_testing_enabled: # Can be removed once jupyter_client >= 6.1 is required.
|
||||
raise SkipTest("AsyncKernelAPITest tests skipped due to down-level jupyter_client!")
|
||||
if sys.version_info < (3, 6): # Can be removed once 3.5 is dropped.
|
||||
raise SkipTest("AsyncKernelAPITest tests skipped due to Python < 3.6!")
|
||||
super(AsyncKernelAPITest, cls).setup_class()
|
||||
|
||||
@classmethod
|
||||
def get_argv(cls):
|
||||
argv = super(AsyncKernelAPITest, cls).get_argv()
|
||||
|
||||
# before we extend the argv with the class, ensure that appropriate jupyter_client is available.
|
||||
# if not available, don't set kernel_manager_class, resulting in the repeat of sync-based tests.
|
||||
if async_testing_enabled:
|
||||
argv.extend(['--NotebookApp.kernel_manager_class='
|
||||
'notebook.services.kernels.kernelmanager.AsyncMappingKernelManager'])
|
||||
return argv
|
||||
|
||||
|
||||
class KernelFilterTest(NotebookTestBase):
|
||||
|
||||
# A special install of NotebookTestBase where only `kernel_info_request`
|
||||
# messages are allowed.
|
||||
config = Config({
|
||||
'NotebookApp': {
|
||||
'MappingKernelManager': {
|
||||
'allowed_message_types': ['kernel_info_request']
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
# Sanity check verifying that the configurable was properly set.
|
||||
def test_config(self):
|
||||
self.assertEqual(self.notebook.kernel_manager.allowed_message_types, ['kernel_info_request'])
|
||||
|
||||
|
||||
class KernelCullingTest(NotebookTestBase):
|
||||
"""Test kernel culling """
|
||||
|
||||
@classmethod
|
||||
def get_argv(cls):
|
||||
argv = super(KernelCullingTest, cls).get_argv()
|
||||
|
||||
# Enable culling with 2s timeout and 1s intervals
|
||||
argv.extend(['--MappingKernelManager.cull_idle_timeout=2',
|
||||
'--MappingKernelManager.cull_interval=1',
|
||||
'--MappingKernelManager.cull_connected=False'])
|
||||
return argv
|
||||
|
||||
def setUp(self):
|
||||
self.kern_api = KernelAPI(self.request,
|
||||
base_url=self.base_url(),
|
||||
headers=self.auth_headers(),
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
for k in self.kern_api.list().json():
|
||||
self.kern_api.shutdown(k['id'])
|
||||
|
||||
def test_culling(self):
|
||||
kid = self.kern_api.start().json()['id']
|
||||
ws = self.kern_api.websocket(kid)
|
||||
model = self.kern_api.get(kid).json()
|
||||
self.assertEqual(model['connections'], 1)
|
||||
assert not self.get_cull_status(kid) # connected, should not be culled
|
||||
ws.close()
|
||||
assert self.get_cull_status(kid) # not connected, should be culled
|
||||
|
||||
def get_cull_status(self, kid):
|
||||
culled = False
|
||||
for i in range(15): # Need max of 3s to ensure culling timeout exceeded
|
||||
try:
|
||||
self.kern_api.get(kid)
|
||||
except HTTPError as e:
|
||||
assert e.response.status_code == 404
|
||||
culled = True
|
||||
break
|
||||
else:
|
||||
time.sleep(0.2)
|
||||
return culled
|
Loading…
Add table
Add a link
Reference in a new issue