Skip to content

Commit a2f72da

Browse files
authored
Merge pull request #2963 from takluyver/shutdown-no-kernels
Config option to shut down server after N seconds with no kernels
2 parents 6f23f45 + 6b0d542 commit a2f72da

File tree

4 files changed

+86
-10
lines changed

4 files changed

+86
-10
lines changed

notebook/notebookapp.py

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@
104104
from jupyter_core.paths import jupyter_runtime_dir, jupyter_path
105105
from notebook._sysinfo import get_sys_info
106106

107-
from ._tz import utcnow
107+
from ._tz import utcnow, utcfromtimestamp
108108
from .utils import url_path_join, check_pid, url_escape
109109

110110
#-----------------------------------------------------------------------------
@@ -336,6 +336,26 @@ def init_handlers(self, settings):
336336
new_handlers.append((r'(.*)', Template404))
337337
return new_handlers
338338

339+
def last_activity(self):
340+
"""Get a UTC timestamp for when the server last did something.
341+
342+
Includes: API activity, kernel activity, kernel shutdown, and terminal
343+
activity.
344+
"""
345+
sources = [
346+
self.settings['started'],
347+
self.settings['kernel_manager'].last_kernel_activity,
348+
]
349+
try:
350+
sources.append(self.settings['api_last_activity'])
351+
except KeyError:
352+
pass
353+
try:
354+
sources.append(self.settings['terminal_last_activity'])
355+
except KeyError:
356+
pass
357+
return max(sources)
358+
339359

340360
class NotebookPasswordApp(JupyterApp):
341361
"""Set a password for the notebook server.
@@ -1139,6 +1159,16 @@ def _update_server_extensions(self, change):
11391159
rate_limit_window = Float(3, config=True, help=_("""(sec) Time window used to
11401160
check the message and data rate limits."""))
11411161

1162+
shutdown_no_activity_timeout = Integer(0, config=True,
1163+
help=("Shut down the server after N seconds with no kernels or "
1164+
"terminals running and no activity. "
1165+
"This can be used together with culling idle kernels "
1166+
"(MappingKernelManager.cull_idle_timeout) to "
1167+
"shutdown the notebook server when it's not in use. This is not "
1168+
"precisely timed: it may shut down up to a minute later. "
1169+
"0 (the default) disables this automatic shutdown.")
1170+
)
1171+
11421172
def parse_command_line(self, argv=None):
11431173
super(NotebookApp, self).parse_command_line(argv)
11441174

@@ -1426,6 +1456,37 @@ def init_mime_overrides(self):
14261456
# mimetype always needs to be text/css, so we override it here.
14271457
mimetypes.add_type('text/css', '.css')
14281458

1459+
1460+
def shutdown_no_activity(self):
1461+
"""Shutdown server on timeout when there are no kernels or terminals."""
1462+
km = self.kernel_manager
1463+
if len(km) != 0:
1464+
return # Kernels still running
1465+
1466+
try:
1467+
term_mgr = self.web_app.settings['terminal_manager']
1468+
except KeyError:
1469+
pass # Terminals not enabled
1470+
else:
1471+
if term_mgr.terminals:
1472+
return # Terminals still running
1473+
1474+
seconds_since_active = \
1475+
(utcnow() - self.web_app.last_activity()).total_seconds()
1476+
self.log.debug("No activity for %d seconds.",
1477+
seconds_since_active)
1478+
if seconds_since_active > self.shutdown_no_activity_timeout:
1479+
self.log.info("No kernels or terminals for %d seconds; shutting down.",
1480+
seconds_since_active)
1481+
self.stop()
1482+
1483+
def init_shutdown_no_activity(self):
1484+
if self.shutdown_no_activity_timeout > 0:
1485+
self.log.info("Will shut down after %d seconds with no kernels or terminals.",
1486+
self.shutdown_no_activity_timeout)
1487+
pc = ioloop.PeriodicCallback(self.shutdown_no_activity, 60000)
1488+
pc.start()
1489+
14291490
@catch_config_error
14301491
def initialize(self, argv=None):
14311492
super(NotebookApp, self).initialize(argv)
@@ -1439,6 +1500,7 @@ def initialize(self, argv=None):
14391500
self.init_signal()
14401501
self.init_server_extensions()
14411502
self.init_mime_overrides()
1503+
self.init_shutdown_no_activity()
14421504

14431505
def cleanup_kernels(self):
14441506
"""Shutdown all kernels.

notebook/services/api/handlers.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,11 @@ class APIStatusHandler(APIHandler):
3333
def get(self):
3434
# if started was missing, use unix epoch
3535
started = self.settings.get('started', utcfromtimestamp(0))
36-
# if we've never seen API activity, use started date
37-
api_last_activity = self.settings.get('api_last_activity', started)
3836
started = isoformat(started)
39-
api_last_activity = isoformat(api_last_activity)
4037

4138
kernels = yield gen.maybe_future(self.kernel_manager.list_kernels())
4239
total_connections = sum(k['connections'] for k in kernels)
43-
last_activity = max(chain([api_last_activity], [k['last_activity'] for k in kernels]))
40+
last_activity = isoformat(self.application.last_activity())
4441
model = {
4542
'started': started,
4643
'last_activity': last_activity,

notebook/services/kernels/kernelmanager.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
# Distributed under the terms of the Modified BSD License.
99

1010
from collections import defaultdict
11+
from datetime import datetime, timedelta
1112
from functools import partial
1213
import os
1314

@@ -17,14 +18,14 @@
1718

1819
from jupyter_client.session import Session
1920
from jupyter_client.multikernelmanager import MultiKernelManager
20-
from traitlets import Any, Bool, Dict, List, Unicode, TraitError, Integer, default, validate
21+
from traitlets import (Any, Bool, Dict, List, Unicode, TraitError, Integer,
22+
Instance, default, validate
23+
)
2124

2225
from notebook.utils import to_os_path, exists
2326
from notebook._tz import utcnow, isoformat
2427
from ipython_genutils.py3compat import getcwd
2528

26-
from datetime import timedelta
27-
2829

2930
class MappingKernelManager(MultiKernelManager):
3031
"""A KernelManager that handles notebook mapping and HTTP error handling"""
@@ -98,6 +99,13 @@ def _update_root_dir(self, proposal):
9899
def _default_kernel_buffers(self):
99100
return defaultdict(lambda: {'buffer': [], 'session_key': '', 'channels': {}})
100101

102+
last_kernel_activity = Instance(datetime,
103+
help="The last activity on any kernel, including shutting down a kernel")
104+
105+
def __init__(self, **kwargs):
106+
super(MappingKernelManager, self).__init__(**kwargs)
107+
self.last_kernel_activity = utcnow()
108+
101109
#-------------------------------------------------------------------------
102110
# Methods for managing kernels and sessions
103111
#-------------------------------------------------------------------------
@@ -255,6 +263,7 @@ def shutdown_kernel(self, kernel_id, now=False):
255263
kernel._activity_stream.close()
256264
self.stop_buffering(kernel_id)
257265
self._kernel_connections.pop(kernel_id, None)
266+
self.last_kernel_activity = utcnow()
258267
return super(MappingKernelManager, self).shutdown_kernel(kernel_id, now=now)
259268

260269
def restart_kernel(self, kernel_id):
@@ -360,7 +369,7 @@ def start_watching_activity(self, kernel_id):
360369

361370
def record_activity(msg_list):
362371
"""Record an IOPub message arriving from a kernel"""
363-
kernel.last_activity = utcnow()
372+
self.last_kernel_activity = kernel.last_activity = utcnow()
364373

365374
idents, fed_msg_list = session.feed_identities(msg_list)
366375
msg = session.deserialize(fed_msg_list)

notebook/terminal/handlers.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from tornado import web
88
import terminado
9+
from notebook._tz import utcnow
910
from ..base.handlers import IPythonHandler
1011
from ..base.zmqhandlers import WebSocketMixin
1112

@@ -31,4 +32,11 @@ def get(self, *args, **kwargs):
3132
if not self.get_current_user():
3233
raise web.HTTPError(403)
3334
return super(TermSocket, self).get(*args, **kwargs)
34-
35+
36+
def on_message(self, message):
37+
super(TermSocket, self).on_message(message)
38+
self.application.settings['terminal_last_activity'] = utcnow()
39+
40+
def write_message(self, message, binary=False):
41+
super(TermSocket, self).write_message(message, binary=binary)
42+
self.application.settings['terminal_last_activity'] = utcnow()

0 commit comments

Comments
 (0)