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 manager for tracking and terminating running server proxies #395

Open
wants to merge 25 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
dc8d4c9
Add server proxy manager
mahendrapaipuri Apr 12, 2023
8351275
Add API handlers to list&del servers from manager
mahendrapaipuri Apr 12, 2023
34a0cba
Add proxy app to manager upon starting
mahendrapaipuri Apr 12, 2023
6b743c9
Add manager for lab extension
mahendrapaipuri Apr 12, 2023
d9be248
Add proxy app svg for creating icon
mahendrapaipuri Apr 12, 2023
3c6cd9a
Add missing arg to extension
mahendrapaipuri Apr 12, 2023
38fc180
Update package.json
mahendrapaipuri Apr 12, 2023
f540620
Use staticfilehandler to serve icons
mahendrapaipuri Apr 12, 2023
3719188
Update server info API URL
mahendrapaipuri Apr 12, 2023
5bab31e
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 12, 2023
d1653d1
Rework manager based on PR comments
mahendrapaipuri Apr 28, 2023
6f30064
Add OpenAPI spec file
mahendrapaipuri Apr 28, 2023
f69a67a
Instantiate manager during extension loading
mahendrapaipuri Apr 28, 2023
97ce111
Add OpenAPI spec file to docs to show REST API
mahendrapaipuri Apr 28, 2023
dafd671
Add note about manager in the Lab UI in docs
mahendrapaipuri Apr 28, 2023
900493b
Change API endpoint in nb extension js file
mahendrapaipuri Apr 28, 2023
c54b956
Rework labextension based on PR comments
mahendrapaipuri Apr 28, 2023
4738d04
Use configurable refreshInterval for polling
mahendrapaipuri Apr 28, 2023
a778a0f
Show warning dialog when app doesnt exist in mnger
mahendrapaipuri Apr 28, 2023
86d0ad9
Add unix_socket to server proxy app model
mahendrapaipuri Apr 28, 2023
bd966ca
Add frontend and backend tests for manager
mahendrapaipuri Apr 28, 2023
bb68e2b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 28, 2023
4ffe4f6
Add note about lab extension settings in docs
mahendrapaipuri Apr 28, 2023
f25996b
Extend Manager from BaseManager
mahendrapaipuri Sep 30, 2023
8987c54
Minor asthetic changes to address review comments
mahendrapaipuri Sep 30, 2023
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
1 change: 1 addition & 0 deletions docs/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@ myst-parser
sphinx-autobuild
sphinx-book-theme
sphinx-copybutton
sphinxcontrib-openapi
sphinxext-opengraph
sphinxext-rediraffe
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions docs/source/api.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.. _api:

=============
REST API
=============

.. openapi:: ../../jupyter_server_proxy/api.yml
:examples:
3 changes: 2 additions & 1 deletion docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@
"sphinx_copybutton",
"sphinxext.opengraph",
"sphinxext.rediraffe",
"sphinxcontrib.openapi",
]
root_doc = "index"
source_suffix = [".md"]
source_suffix = [".md", ".rst"]
exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]


Expand Down
1 change: 1 addition & 0 deletions docs/source/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ install
server-process
launchers
arbitrary-ports-hosts
api
```

## Convenience packages for popular applications
Expand Down
33 changes: 33 additions & 0 deletions docs/source/launchers.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,36 @@ buttons in JupyterLab's Launcher panel for registered server processes.
```

Clicking on them opens the proxied application in a new browser window.

Currently running proxy applications are shown in the running sessions tab
under "Server Proxy Apps" section.

```{image} _static/images/labextension-manager.gif

```

As shown in the GIF, users can consult the metadata of each running proxy
application by hovering over the name of the proxy. It is also possible to
terminate the proxy application using Shut Down button.

```{note}
When the user clicks Shut Down button to terminate a proxy application,
a `SIGTERM` signal is sent to the application. It is the user's responsibility
to ensure that the application exits cleanly with a `SIGTERM` signal. There
are certain applications (like MLflow) that cannot be terminted with `SIGTERM`
signal and in those cases, the users can setup wrapper scripts to trap the
signal and ensure clean teardown of the application.
```

The lab extension manager will poll for running proxy applications at a
given interval which can be configured using Jupyter Server Proxy settings.
By default this is set to 10 seconds. Users can change
this interval by changing `Auto-refresh rate` in `Jupyter Server Proxy`
section in `Advanced Settings Editor` in JupyterLab UI.

```{image} _static/images/labextension-settings.png

```

Only proxy applications that are started by `jupyter-server-proxy` are shown
in the running Server Proxy Apps section.
43 changes: 25 additions & 18 deletions jupyter_server_proxy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from jupyter_server.utils import url_path_join as ujoin
import traitlets

from .api import IconHandler, ServersInfoHandler
from .api import setup_api_handlers
from .config import ServerProxy as ServerProxyConfig
from .config import get_entrypoint_server_processes, make_handlers, make_server_process
from .handlers import setup_handlers
from .manager import ServerProxyAppManager


# Jupyter Extension points
Expand Down Expand Up @@ -38,14 +39,31 @@ def _jupyter_labextension_paths():
def _load_jupyter_server_extension(nbapp):
# Set up handlers picked up via config
base_url = nbapp.web_app.settings["base_url"]

# Add server_proxy_manager trait to ServerApp and Instantiate a manager
nbapp.add_traits(server_proxy_manager=traitlets.Instance(ServerProxyAppManager))
manager = nbapp.server_proxy_manager = ServerProxyAppManager()
serverproxy_config = ServerProxyConfig(parent=nbapp)

# Add a long running background task that monitors the running proxies
try:
nbapp.io_loop.call_later(
serverproxy_config.monitor_interval,
manager.monitor,
serverproxy_config.monitor_interval,
)
except AttributeError:
nbapp.log.debug(
"[jupyter-server-proxy] Server proxy manager is only supportted "
"for Notebook >= 7",
)

server_processes = [
make_server_process(name, server_process_config, serverproxy_config)
for name, server_process_config in serverproxy_config.servers.items()
]
server_processes += get_entrypoint_server_processes(serverproxy_config)
server_handlers = make_handlers(base_url, server_processes)
server_handlers = make_handlers(base_url, manager, server_processes)
nbapp.web_app.add_handlers(".*", server_handlers)

# Set up default non-server handler
Expand All @@ -54,21 +72,10 @@ def _load_jupyter_server_extension(nbapp):
serverproxy_config,
)

icons = {}
for sp in server_processes:
if sp.launcher_entry.enabled and sp.launcher_entry.icon_path:
icons[sp.name] = sp.launcher_entry.icon_path

nbapp.web_app.add_handlers(
".*",
[
(
ujoin(base_url, "server-proxy/servers-info"),
ServersInfoHandler,
{"server_processes": server_processes},
),
(ujoin(base_url, "server-proxy/icon/(.*)"), IconHandler, {"icons": icons}),
],
setup_api_handlers(
nbapp.web_app,
manager,
server_processes,
)

nbapp.log.debug(
Expand Down
119 changes: 87 additions & 32 deletions jupyter_server_proxy/api.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import mimetypes
import json

from jupyter_server.base.handlers import JupyterHandler
from jupyter_server.utils import url_path_join as ujoin
Expand Down Expand Up @@ -34,39 +34,94 @@ async def get(self):
self.write({"server_processes": data})


# FIXME: Should be a StaticFileHandler subclass
class IconHandler(JupyterHandler):
"""
Serve launcher icons
"""
# IconHandler has been copied from JupyterHub's IconHandler:
# https://github.com/jupyterhub/jupyterhub/blob/4.0.0b2/jupyterhub/handlers/static.py#L22-L31
class ServersIconHandler(web.StaticFileHandler):
"""A singular handler for serving the icon."""

def initialize(self, icons):
"""
icons is a dict of titles to paths
"""
self.icons = icons
def get(self):
return super().get("")

@classmethod
def get_absolute_path(cls, root, path):
"""We only serve one file, ignore relative path"""
import os

return os.path.abspath(root)


class ServersAPIHandler(JupyterHandler):
"""Handler to get metadata or terminate of a given server or all servers"""

def initialize(self, manager):
self.manager = manager

@web.authenticated
async def delete(self, name):
"""Delete a server proxy by name"""
if not name:
raise web.HTTPError(
403,
"Please set the name of a running server proxy that "
"user wishes to terminate",
)

try:
val = await self.manager.terminate_server_proxy_app(name)
if val is None:
raise Exception(
f"Proxy {name} not found. Are you sure the {name} "
f"is managed by jupyter-server-proxy?"
)
else:
self.set_status(204)
self.finish()
except Exception as e:
raise web.HTTPError(404, str(e))

@web.authenticated
async def get(self, name):
if name not in self.icons:
raise web.HTTPError(404)
path = self.icons[name]

# Guess mimetype appropriately
# Stolen from https://github.com/tornadoweb/tornado/blob/b399a9d19c45951e4561e6e580d7e8cf396ef9ff/tornado/web.py#L2881
mime_type, encoding = mimetypes.guess_type(path)
if encoding == "gzip":
content_type = "application/gzip"
# As of 2015-07-21 there is no bzip2 encoding defined at
# http://www.iana.org/assignments/media-types/media-types.xhtml
# So for that (and any other encoding), use octet-stream.
elif encoding is not None:
content_type = "application/octet-stream"
elif mime_type is not None:
content_type = mime_type
# if mime_type not detected, use application/octet-stream
"""Get meta data of a running server proxy"""
if name:
apps = self.manager.get_server_proxy_app(name)._asdict()
# If no server proxy found this will be a dict with empty values
if not apps["name"]:
raise web.HTTPError(404, f"Server proxy {name} not found")
else:
content_type = "application/octet-stream"
apps = [app._asdict() for app in self.manager.list_server_proxy_apps()]

self.set_status(200)
self.finish(json.dumps(apps))


def setup_api_handlers(web_app, manager, server_processes):
base_url = web_app.settings["base_url"]

# Make a list of icon handlers
icon_handlers = []
for sp in server_processes:
if sp.launcher_entry.enabled and sp.launcher_entry.icon_path:
icon_handlers.append(
(
ujoin(base_url, f"server-proxy/icon/{sp.name}"),
ServersIconHandler,
{"path": sp.launcher_entry.icon_path},
)
)

with open(self.icons[name]) as f:
self.write(f.read())
self.set_header("Content-Type", content_type)
web_app.add_handlers(
".*",
[
(
ujoin(base_url, "server-proxy/api/servers-info"),
ServersInfoHandler,
{"server_processes": server_processes},
),
(
ujoin(base_url, r"server-proxy/api/servers/(?P<name>.*)"),
ServersAPIHandler,
{"manager": manager},
),
]
+ icon_handlers,
)
Loading