Uploaded Test files

This commit is contained in:
Batuhan Berk Başoğlu 2020-11-12 11:05:57 -05:00
parent f584ad9d97
commit 2e81cb7d99
16627 changed files with 2065359 additions and 102444 deletions

View 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),
]

View file

@ -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)

View file

@ -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