Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add activity watching to kernels #1827

Merged
merged 9 commits into from
Jan 24, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions notebook/_tz.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# encoding: utf-8
"""
Timezone utilities

Just UTC-awareness right now
"""

# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

from datetime import tzinfo, timedelta, datetime

# constant for zero offset
ZERO = timedelta(0)

class tzUTC(tzinfo):
"""tzinfo object for UTC (zero offset)"""

def utcoffset(self, d):
return ZERO

def dst(self, d):
return ZERO

UTC = tzUTC()

def utc_aware(unaware):
"""decorator for adding UTC tzinfo to datetime's utcfoo methods"""
def utc_method(*args, **kwargs):
dt = unaware(*args, **kwargs)
return dt.replace(tzinfo=UTC)
return utc_method

utcfromtimestamp = utc_aware(datetime.utcfromtimestamp)
utcnow = utc_aware(datetime.utcnow)

def isoformat(dt):
"""Return iso-formatted timestamp

Like .isoformat(), but uses Z for UTC instead of +00:00
"""
return dt.isoformat().replace('+00:00', 'Z')
13 changes: 12 additions & 1 deletion notebook/base/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from ipython_genutils.py3compat import string_types

import notebook
from notebook._tz import utcnow
from notebook.utils import is_hidden, url_path_join, url_is_absolute, url_escape
from notebook.services.security import csp_report_uri

Expand Down Expand Up @@ -432,8 +433,18 @@ def content_security_policy(self):
"default-src 'none'",
])
return csp


# set _track_activity = False on API handlers that shouldn't track activity
_track_activity = True

def update_api_activity(self):
"""Update last_activity of API requests"""
# record activity of authenticated requests
if self._track_activity and self.get_current_user():
self.settings['api_last_activity'] = utcnow()

def finish(self, *args, **kwargs):
self.update_api_activity()
self.set_header('Content-Type', 'application/json')
return super(APIHandler, self).finish(*args, **kwargs)

Expand Down
6 changes: 5 additions & 1 deletion notebook/notebookapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@
from jupyter_core.paths import jupyter_runtime_dir, jupyter_path
from notebook._sysinfo import get_sys_info

from ._tz import utcnow
from .utils import url_path_join, check_pid, url_escape

#-----------------------------------------------------------------------------
Expand Down Expand Up @@ -198,6 +199,8 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager,
working on the notebook's Javascript and LESS""")
warnings.warn("The `ignore_minified_js` flag is deprecated and will be removed in Notebook 6.0", DeprecationWarning)

now = utcnow()

settings = dict(
# basics
log_function=log_request,
Expand Down Expand Up @@ -236,7 +239,8 @@ def init_settings(self, jupyter_app, kernel_manager, contents_manager,
kernel_spec_manager=kernel_spec_manager,
config_manager=config_manager,

# IPython stuff
# Jupyter stuff
started=now,
jinja_template_vars=jupyter_app.jinja_template_vars,
nbextensions_path=jupyter_app.nbextensions_path,
websocket_url=jupyter_app.websocket_url,
Expand Down
46 changes: 46 additions & 0 deletions notebook/services/api/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -617,7 +617,36 @@ paths:



/status:
get:
summary: Get the current status / activity of the server
responses:
200:
description: The current status of the server
$ref: '#/definitions/APIStatus'
definitions:
APIStatus:
description: |
Notebook server API status.
Added in notebook 5.0.
properties:
started:
type: string
description: |
ISO8601 timestamp indicating when the notebook server started.
last_activity:
type: string
description: |
ISO8601 timestamp indicating the last activity on the server,
either on the REST API or kernel activity.
connections:
type: number
description: |
The total number of currently open connections to kernels.
kernels:
type: number
description: |
The total number of running kernels.
KernelSpec:
description: Kernel spec (contents of kernel.json)
properties:
Expand Down Expand Up @@ -697,6 +726,23 @@ definitions:
name:
type: string
description: kernel spec name
last_activity:
type: string
description: |
ISO 8601 timestamp for the last-seen activity on this kernel.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this explicitly be UTC?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are explicitly timezone-aware, and UTC as an implementation detail. I'll add that detail, though.

Use this in combination with execution_state == 'idle' to identify
which kernels have been idle since a given time.
Timestamps will be UTC, indicated 'Z' suffix.
Added in notebook server 5.0.
connections:
type: number
description: |
The number of active connections to this kernel.
execution_state:
type: string
description: |
Current execution state of the kernel (typically 'idle' or 'busy', but may be other values, such as 'starting').
Added in notebook server 5.0.
Session:
description: A session
type: object
Expand Down
39 changes: 36 additions & 3 deletions notebook/services/api/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.

from tornado import web
from ...base.handlers import IPythonHandler
from itertools import chain
import json

from tornado import gen, web

from ...base.handlers import IPythonHandler, APIHandler, json_errors
from notebook._tz import utcfromtimestamp, isoformat

import os

class APISpecHandler(web.StaticFileHandler, IPythonHandler):
Expand All @@ -18,6 +24,33 @@ def get(self):
self.set_header('Content-Type', 'text/x-yaml')
return web.StaticFileHandler.get(self, 'api.yaml')

class APIStatusHandler(APIHandler):

_track_activity = False

@json_errors
@web.authenticated
@gen.coroutine
def get(self):
# if started was missing, use unix epoch
started = self.settings.get('started', utcfromtimestamp(0))
# if we've never seen API activity, use started date
api_last_activity = self.settings.get('api_last_activity', started)
started = isoformat(started)
api_last_activity = isoformat(api_last_activity)

kernels = yield gen.maybe_future(self.kernel_manager.list_kernels())
total_connections = sum(k['connections'] for k in kernels)
last_activity = max(chain([api_last_activity], [k['last_activity'] for k in kernels]))
model = {
'started': started,
'last_activity': last_activity,
'kernels': len(kernels),
'connections': total_connections,
}
self.finish(json.dumps(model, sort_keys=True))

default_handlers = [
(r"/api/spec.yaml", APISpecHandler)
(r"/api/spec.yaml", APISpecHandler),
(r"/api/status", APIStatusHandler),
]
Empty file.
32 changes: 32 additions & 0 deletions notebook/services/api/tests/test_api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"""Test the basic /api endpoints"""

import requests

from notebook._tz import isoformat
from notebook.utils import url_path_join
from notebook.tests.launchnotebook import NotebookTestBase


class KernelAPITest(NotebookTestBase):
"""Test the kernels web service API"""

def _req(self, verb, path, **kwargs):
r = self.request(verb, url_path_join('api', path))
r.raise_for_status()
return r

def get(self, path, **kwargs):
return self._req('GET', path)

def test_get_spec(self):
r = self.get('spec.yaml')
assert r.text

def test_get_status(self):
r = self.get('status')
data = r.json()
assert data['connections'] == 0
assert data['kernels'] == 0
assert data['last_activity'].endswith('Z')
assert data['started'].endswith('Z')
assert data['started'] == isoformat(self.notebook.web_app.settings['started'])
3 changes: 2 additions & 1 deletion notebook/services/contents/filecheckpoints.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@
)
from .fileio import FileManagerMixin

from . import tz
from ipython_genutils.path import ensure_dir_exists
from ipython_genutils.py3compat import getcwd
from traitlets import Unicode

from notebook import _tz as tz


class FileCheckpoints(FileManagerMixin, Checkpoints):
"""
Expand Down
3 changes: 2 additions & 1 deletion notebook/services/contents/filemanager.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@
from ipython_genutils.importstring import import_item
from traitlets import Any, Unicode, Bool, TraitError, observe, default, validate
from ipython_genutils.py3compat import getcwd, string_types
from . import tz

from notebook import _tz as tz
from notebook.utils import (
is_hidden, is_file_hidden,
to_api_path,
Expand Down
46 changes: 0 additions & 46 deletions notebook/services/contents/tz.py

This file was deleted.

10 changes: 6 additions & 4 deletions notebook/services/kernels/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ class MainKernelHandler(APIHandler):
def get(self):
km = self.kernel_manager
kernels = yield gen.maybe_future(km.list_kernels())
self.finish(json.dumps(kernels))
self.finish(json.dumps(kernels, default=date_default))

@json_errors
@web.authenticated
Expand All @@ -51,7 +51,7 @@ def post(self):
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))
self.finish(json.dumps(model, default=date_default))


class KernelHandler(APIHandler):
Expand All @@ -62,7 +62,7 @@ def get(self, kernel_id):
km = self.kernel_manager
km._check_kernel_id(kernel_id)
model = km.kernel_model(kernel_id)
self.finish(json.dumps(model))
self.finish(json.dumps(model, default=date_default))

@json_errors
@web.authenticated
Expand Down Expand Up @@ -93,7 +93,7 @@ def post(self, kernel_id, action):
self.set_status(500)
else:
model = km.kernel_model(kernel_id)
self.write(json.dumps(model))
self.write(json.dumps(model, default=date_default))
self.finish()


Expand Down Expand Up @@ -260,6 +260,7 @@ def _register_session(self):

def open(self, kernel_id):
super(ZMQChannelsHandler, self).open()
self.kernel_manager.notify_connect(kernel_id)
try:
self.create_stream()
except web.HTTPError as e:
Expand Down Expand Up @@ -401,6 +402,7 @@ def on_close(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,
)
Expand Down
Loading