Skip to content

Commit 085ff47

Browse files
Add server proxy manager
It keeps simpervisor proc objects in internal state to be able to teminate a process by calling terminate method on proc object. It also checks the status of active processes by running a periodic callback. If a process is killed outside of jsp, it removes it from running apps list.
1 parent 3dde433 commit 085ff47

File tree

2 files changed

+136
-0
lines changed

2 files changed

+136
-0
lines changed

jupyter_server_proxy/manager.py

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
"""Manager for jupyter server proxy"""
2+
3+
from collections import namedtuple
4+
from tornado.ioloop import PeriodicCallback
5+
from jupyter_server.utils import url_path_join as ujoin
6+
7+
from .utils import check_pid
8+
9+
10+
ServerProxy = namedtuple('ServerProxy', [
11+
'name', 'url', 'cmd', 'port', 'managed'
12+
])
13+
ServerProxyProc = namedtuple('ServerProxyProc', [
14+
'name', 'proc'
15+
])
16+
17+
18+
async def monitor_server_proxy_procs():
19+
"""Perodically monitor the server proxy processes. If user terminates
20+
the process outside of jupyter-server-proxy, we should be able to
21+
capture that and remove proxy app from manager"""
22+
# Get current active apps
23+
procs = manager._list_server_proxy_procs()
24+
25+
# Check if all pids are alive
26+
for proc in procs:
27+
exists = check_pid(proc.proc.pid)
28+
if not exists:
29+
await manager.del_server_proxy_app(proc.name)
30+
31+
32+
class ServerProxyAppManager:
33+
"""
34+
A class for listing and stopping server proxies that are started
35+
by jupyter server proxy.
36+
"""
37+
38+
def __init__(self):
39+
"""Initialize the server proxy manager"""
40+
# List of server proxy apps
41+
self.server_proxy_apps = []
42+
43+
# List of server proxy app proc objects. For internal use only
44+
self._server_proxy_procs = []
45+
46+
# Total number of currently running proxy apps
47+
self.num_active_server_proxy_apps = 0
48+
49+
async def add_server_proxy_app(self, name, base_url, cmd, port, proc):
50+
"""Add a launched proxy server to list"""
51+
self.num_active_server_proxy_apps += 1
52+
53+
# Add proxy server metadata
54+
self.server_proxy_apps.append(
55+
ServerProxy(
56+
name=name,
57+
url=ujoin(base_url, name),
58+
cmd=' '.join(cmd),
59+
port=port,
60+
managed=True if proc else False,
61+
))
62+
63+
# Add proxy server proc object so that we can send SIGTERM
64+
# when user chooses to shut it down
65+
self._server_proxy_procs.append(
66+
ServerProxyProc(
67+
name=name,
68+
proc=proc,
69+
))
70+
71+
async def del_server_proxy_app(self, name):
72+
"""Remove a launched proxy server from list"""
73+
self.server_proxy_apps = [app for app in self.server_proxy_apps if app.name != name]
74+
self._server_proxy_procs = [app for app in self._server_proxy_procs if app.name != name]
75+
self.num_active_server_proxy_apps -= 1
76+
77+
def get_server_proxy_app(self, name):
78+
"""Get a given server proxy app"""
79+
return next((app for app in self.server_proxy_apps if app.name == name), {})
80+
81+
def _get_server_proxy_proc(self, name):
82+
"""Get a given server proxy app"""
83+
return next((app for app in self._server_proxy_procs if app.name == name), {})
84+
85+
def list_server_proxy_apps(self):
86+
"""List all active server proxy apps"""
87+
return self.server_proxy_apps
88+
89+
def _list_server_proxy_procs(self):
90+
"""List all active server proxy proc objs"""
91+
return self._server_proxy_procs
92+
93+
async def terminate_server_proxy_app(self, name):
94+
"""Terminate a server proxy by sending SIGTERM"""
95+
app = self._get_server_proxy_proc(name)
96+
try:
97+
# Here we send SIGTERM signal to terminate proxy app
98+
# graciously so we can restart it if needed. Note that
99+
# some servers may not get stopped by sending SIGTERM
100+
# signal (example is mlflow server). In this case, it is
101+
# user's responsibility to write wrapper scripts around
102+
# proxy app's executable to terminate them cleanly using
103+
# TERM signal. It is also important to set exit code to 0
104+
# when using such wrappers when proxy apps shutdown.
105+
await app.proc.terminate()
106+
107+
# Remove proxy app from list
108+
await self.del_server_proxy_app(name)
109+
110+
return True
111+
except (KeyError, AttributeError):
112+
return None
113+
114+
async def terminate_all(self):
115+
"""Close all server proxy and cleanup"""
116+
for app in self.server_proxy_apps:
117+
await self.terminate_server_proxy_app(app)
118+
119+
120+
# Create a default manager to keep track of server proxy apps.
121+
manager = ServerProxyAppManager()
122+
123+
# Create a Periodic call back function to check the status of processes
124+
pc = PeriodicCallback(monitor_server_proxy_procs, 1e4)
125+
pc.start()

jupyter_server_proxy/utils.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
from traitlets import TraitType
23

34

@@ -49,3 +50,13 @@ def validate(self, obj, value):
4950
return value
5051
else:
5152
self.error(obj, value)
53+
54+
55+
def check_pid(pid):
56+
""" Check For the existence of a unix pid"""
57+
try:
58+
os.kill(pid, 0)
59+
except OSError:
60+
return False
61+
else:
62+
return True

0 commit comments

Comments
 (0)