diff --git a/notebook/notebookapp.py b/notebook/notebookapp.py index 5e73c504a..cad7b7aae 100755 --- a/notebook/notebookapp.py +++ b/notebook/notebookapp.py @@ -1070,6 +1070,14 @@ class NotebookApp(JupyterApp): rate_limit_window = Float(3, config=True, help=_("""(sec) Time window used to check the message and data rate limits.""")) + shutdown_no_kernels_timeout = Integer(0, config=True, + help=("Use a positive integer, to shut down the server after N " + "seconds with no kernels running. This can be used together with " + "culling idle kernels (MappingKernelManager.cull_idle_timeout) to " + "shutdown the notebook server when it's not in use. " + "Do not rely on this being accurately timed.") + ) + def parse_command_line(self, argv=None): super(NotebookApp, self).parse_command_line(argv) @@ -1357,6 +1365,26 @@ class NotebookApp(JupyterApp): # mimetype always needs to be text/css, so we override it here. mimetypes.add_type('text/css', '.css') + def shutdown_no_kernels(self): + """Shutdown server on timeout when there are no kernels.""" + km = self.kernel_manager + if len(km) == 0: + seconds_since_kernel = \ + (utcnow() - km.last_kernel_activity).total_seconds() + self.log.debug("No kernels for %d seconds.", + seconds_since_kernel) + if seconds_since_kernel > self.shutdown_no_kernels_timeout: + self.log.info("No kernels for %d seconds; shutting down.", + seconds_since_kernel) + self.stop() + + def init_shutdown_no_kernels(self): + if self.shutdown_no_kernels_timeout > 0: + self.log.info("Will shut down after %d seconds with no kernels.", + self.shutdown_no_kernels_timeout) + pc = ioloop.PeriodicCallback(self.shutdown_no_kernels, 60000) + pc.start() + @catch_config_error def initialize(self, argv=None): super(NotebookApp, self).initialize(argv) @@ -1370,6 +1398,7 @@ class NotebookApp(JupyterApp): self.init_signal() self.init_server_extensions() self.init_mime_overrides() + self.init_shutdown_no_kernels() def cleanup_kernels(self): """Shutdown all kernels. diff --git a/notebook/services/kernels/kernelmanager.py b/notebook/services/kernels/kernelmanager.py index 6f47ed6a7..a2988d281 100644 --- a/notebook/services/kernels/kernelmanager.py +++ b/notebook/services/kernels/kernelmanager.py @@ -8,6 +8,7 @@ # Distributed under the terms of the Modified BSD License. from collections import defaultdict +from datetime import datetime, timedelta from functools import partial import os @@ -17,14 +18,14 @@ 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, default, validate +from traitlets import (Any, Bool, Dict, List, Unicode, TraitError, Integer, + Instance, default, validate +) from notebook.utils import to_os_path, exists from notebook._tz import utcnow, isoformat from ipython_genutils.py3compat import getcwd -from datetime import timedelta - class MappingKernelManager(MultiKernelManager): """A KernelManager that handles notebook mapping and HTTP error handling""" @@ -88,6 +89,13 @@ class MappingKernelManager(MultiKernelManager): 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") + + def __init__(self, **kwargs): + super(MappingKernelManager, self).__init__(**kwargs) + self.last_kernel_activity = utcnow() + #------------------------------------------------------------------------- # Methods for managing kernels and sessions #------------------------------------------------------------------------- @@ -241,6 +249,7 @@ class MappingKernelManager(MultiKernelManager): kernel._activity_stream.close() self.stop_buffering(kernel_id) self._kernel_connections.pop(kernel_id, None) + self.last_kernel_activity = utcnow() return super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now) def restart_kernel(self, kernel_id): @@ -346,7 +355,7 @@ class MappingKernelManager(MultiKernelManager): def record_activity(msg_list): """Record an IOPub message arriving from a kernel""" - kernel.last_activity = utcnow() + self.last_kernel_activity = kernel.last_activity = utcnow() idents, fed_msg_list = session.feed_identities(msg_list) msg = session.deserialize(fed_msg_list)