Skip to content

Commit 087c11d

Browse files
committed
Send Activity Notifications to JupyterHub
1 parent 36c8e17 commit 087c11d

File tree

3 files changed

+84
-90
lines changed

3 files changed

+84
-90
lines changed

jupyter_server_proxy/standalone/__init__.py

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,8 @@
66
from tornado.httpserver import HTTPServer
77
from tornado.log import app_log
88

9-
from .proxy import (
10-
configure_http_client,
11-
get_port_from_env,
12-
get_ssl_options,
13-
make_app,
14-
start_keep_alive,
15-
)
9+
from .activity import start_activity_update
10+
from .proxy import configure_http_client, get_port_from_env, get_ssl_options, make_app
1611

1712

1813
def run(
@@ -24,8 +19,7 @@ def run(
2419
logs=True,
2520
authtype="oauth",
2621
timeout=60,
27-
last_activity_interval=300,
28-
force_alive=True,
22+
activity_interval=300,
2923
progressive=False,
3024
websocket_max_message_size=0,
3125
):
@@ -69,8 +63,8 @@ def run(
6963
print(f"Auth Type: {authtype}")
7064
print(f"Command: {command}")
7165

72-
if last_activity_interval > 0:
73-
start_keep_alive(last_activity_interval, force_alive, app.settings)
66+
if activity_interval > 0:
67+
start_activity_update(activity_interval)
7468

7569
ioloop.IOLoop.current().start()
7670

@@ -117,16 +111,10 @@ def main():
117111
help="Timeout to wait until the subprocess has started and can be addressed.",
118112
)
119113
parser.add_argument(
120-
"--last-activity-interval",
114+
"--activity-interval",
121115
default=300,
122116
type=int,
123-
help="Frequency to notify Hub that the WebApp is still running in seconds. 0 for never.",
124-
)
125-
parser.add_argument(
126-
"--force-alive",
127-
action="store_true",
128-
default=True,
129-
help="Always report, that there has been activity (force keep alive) - only if last-activity-interval > 0.",
117+
help="Frequency to notify Hub that the WebApp is still running (In seconds, 0 for never).",
130118
)
131119
parser.add_argument(
132120
"--progressive",
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import json
2+
import os
3+
from datetime import datetime
4+
5+
from jupyterhub.utils import exponential_backoff, isoformat
6+
from tornado import httpclient, ioloop
7+
from tornado.log import app_log as log
8+
9+
10+
async def notify_activity():
11+
"""
12+
Regularly notify JupyterHub of activity.
13+
See `jupyrehub/singleuser/extensions#L396`
14+
"""
15+
16+
client = httpclient.AsyncHTTPClient()
17+
last_activity_timestamp = isoformat(datetime.utcnow())
18+
failure_count = 0
19+
20+
activity_url = os.environ.get("JUPYTERHUB_ACTIVITY_URL")
21+
server_name = os.environ.get("JUPYTERHUB_SERVER_NAME")
22+
api_token = os.environ.get("JUPYTERHUB_API_TOKEN")
23+
24+
if not (activity_url and server_name and api_token):
25+
log.error(
26+
"Could not find environment variables to send notification to JupyterHub"
27+
)
28+
return
29+
30+
async def notify():
31+
"""Send Notification, return if successful"""
32+
nonlocal failure_count
33+
log.debug(f"Notifying Hub of activity {last_activity_timestamp}")
34+
35+
req = httpclient.HTTPRequest(
36+
url=activity_url,
37+
method="POST",
38+
headers={
39+
"Authorization": f"token {api_token}",
40+
"Content-Type": "application/json",
41+
},
42+
body=json.dumps(
43+
{
44+
"servers": {
45+
server_name: {"last_activity": last_activity_timestamp}
46+
},
47+
"last_activity": last_activity_timestamp,
48+
}
49+
),
50+
)
51+
52+
try:
53+
await client.fetch(req)
54+
return True
55+
except httpclient.HTTPError as e:
56+
failure_count += 1
57+
log.error(f"Error notifying Hub of activity: {e}")
58+
return False
59+
60+
# Try sending notification for 1 minute
61+
await exponential_backoff(
62+
notify,
63+
fail_message="Failed to notify Hub of activity",
64+
start_wait=1,
65+
max_wait=15,
66+
timeout=60,
67+
)
68+
69+
if failure_count > 0:
70+
log.info(f"Sent hub activity after {failure_count} retries")
71+
72+
73+
def start_activity_update(interval):
74+
pc = ioloop.PeriodicCallback(notify_activity, 1e3 * interval, 0.1)
75+
pc.start()

jupyter_server_proxy/standalone/proxy.py

Lines changed: 2 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import json
21
import os
32
import re
4-
from datetime import datetime
53
from urllib.parse import urlparse
64

75
from jupyterhub import __version__ as __jh_version__
86
from jupyterhub.services.auth import HubOAuthCallbackHandler
9-
from jupyterhub.utils import exponential_backoff, isoformat, make_ssl_context
10-
from tornado import httpclient, ioloop
7+
from jupyterhub.utils import make_ssl_context
8+
from tornado import httpclient
119
from tornado.web import Application, RedirectHandler, RequestHandler
1210
from tornado.websocket import WebSocketHandler
1311

@@ -183,70 +181,3 @@ def get_port_from_env():
183181
elif url.scheme == "https":
184182
return 443
185183
return 8888
186-
187-
188-
def start_keep_alive(last_activity_interval, force_alive, settings):
189-
client = httpclient.AsyncHTTPClient()
190-
191-
hub_activity_url = os.environ.get("JUPYTERHUB_ACTIVITY_URL", "")
192-
server_name = os.environ.get("JUPYTERHUB_SERVER_NAME", "")
193-
api_token = os.environ.get("JUPYTERHUB_API_TOKEN", "")
194-
195-
if api_token == "" or server_name == "" or hub_activity_url == "":
196-
print(
197-
"The following env vars are required to report activity back to the hub for keep alive: "
198-
"JUPYTERHUB_ACTIVITY_URL ({}), JUPYTERHUB_SERVER_NAME({})".format(
199-
hub_activity_url, server_name
200-
)
201-
)
202-
return
203-
204-
async def send_activity():
205-
async def notify():
206-
print("About to notify Hub of activity")
207-
208-
last_activity_timestamp = None
209-
210-
if force_alive:
211-
last_activity_timestamp = datetime.utcnow()
212-
else:
213-
last_activity_timestamp = settings.get("api_last_activity", None)
214-
215-
if last_activity_timestamp:
216-
last_activity_timestamp = isoformat(last_activity_timestamp)
217-
req = httpclient.HTTPRequest(
218-
url=hub_activity_url,
219-
method="POST",
220-
headers={
221-
"Authorization": f"token {api_token}",
222-
"Content-Type": "application/json",
223-
},
224-
body=json.dumps(
225-
{
226-
"servers": {
227-
server_name: {"last_activity": last_activity_timestamp}
228-
},
229-
"last_activity": last_activity_timestamp,
230-
}
231-
),
232-
)
233-
try:
234-
await client.fetch(req)
235-
except Exception as e:
236-
print(f"Error notifying Hub of activity: {e}")
237-
return False
238-
else:
239-
return True
240-
241-
return True # Nothing to report, so really it worked
242-
243-
await exponential_backoff(
244-
notify,
245-
fail_message="Failed to notify Hub of activity",
246-
start_wait=1,
247-
max_wait=15,
248-
timeout=60,
249-
)
250-
251-
pc = ioloop.PeriodicCallback(send_activity, 1e3 * last_activity_interval, 0.1)
252-
pc.start()

0 commit comments

Comments
 (0)