Skip to content

Commit d167159

Browse files
amazingarmanArmanShah
andauthored
Added new WebhookKernelSessionManager for Kernel Persistence (#1101)
* Added new WebhookKernelSessionManager * Removed unused imports * Clean up Co-authored-by: ArmanShah <[email protected]>
1 parent 9c49f08 commit d167159

File tree

5 files changed

+223
-3
lines changed

5 files changed

+223
-3
lines changed

docs/source/operators/config-cli.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,29 @@ FileKernelSessionManager(KernelSessionManager) options
283283
reside. This directory should exist. (EG_PERSISTENCE_ROOT env var)
284284
Default: ''
285285
286+
WebhookKernelSessionManager(KernelSessionManager) options
287+
---------------------------------------------------------
288+
--WebhookKernelSessionManager.enable_persistence=<Bool>
289+
Enable kernel session persistence (True or False). Default = False
290+
(EG_KERNEL_SESSION_PERSISTENCE env var)
291+
Default: False
292+
--WebhookKernelSessionManager.persistence_root=<Unicode>
293+
Identifies the root 'directory' under which the 'kernel_sessions' node will
294+
reside. This directory should exist. (EG_PERSISTENCE_ROOT env var)
295+
Default: None
296+
--WebhookKernelSessionManager.webhook_url=<Unicode>
297+
URL endpoint for webhook kernel session manager
298+
Default: None
299+
--WebhookKernelSessionManager.auth_type=<Unicode>
300+
Authentication type for webhook kernel session manager API. Either basic, digest or None
301+
Default: None
302+
--WebhookKernelSessionManager.webhook_username=<Unicode>
303+
Username for webhook kernel session manager API auth
304+
Default: None
305+
--WebhookKernelSessionManager.webhook_password=<Unicode>
306+
Password for webhook kernel session manager API auth
307+
Default: None
308+
286309
RemoteMappingKernelManager(AsyncMappingKernelManager) options
287310
-------------------------------------------------------------
288311
--RemoteMappingKernelManager.allowed_message_types=<list-item-1>...
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# Kernel Session Persistence
2+
3+
Enabling kernel session persistence allows Jupyter Notebooks to reconnect to kernels when Enterprise Gateway is restarted. There are two ways of persisting kernel sessions: _File Kernel Session Persistence_ and _Webhook Kernel Session Persistence_.
4+
5+
NOTE: Kernel Session Persistence should be considered experimental!
6+
7+
## File Kernel Session Persistence
8+
9+
File Kernel Session Persistence stores all kernel sessions as a file in a specified directory. To enable this, set the environment variable `EG_KERNEL_SESSION_PERSISTENCE=True` or configure `FileKernelSessionManager.enable_persistence=True`. To change the directory in which the kernel session file is being saved, either set the environment variable `EG_PERSISTENCE_ROOT` or configure `FileKernelSessionManager.persistence_root` to the directory.
10+
11+
## Webhook Kernel Session Persistence
12+
13+
Webhook Kernel Session Persistence stores all kernel sessions to any database. In order for this to work, an API must be created. The API must include four endpoints:
14+
15+
- A GET that will retrieve a list of all kernel sessions from a database
16+
- A GET that will take the kernel id as a path variable and retrieve that information from a database
17+
- A DELETE that will delete all kernel sessions, where the body of the request is a list of kernel ids
18+
- A POST that will take kernel id as a path variable and kernel session in the body of the request and save it to a database where the object being saved is:
19+
20+
```
21+
{
22+
kernel_id: UUID string,
23+
kernel_session: JSON
24+
}
25+
```
26+
27+
To enable the webhook kernel session persistence, set the environment variable `EG_KERNEL_SESSION_PERSISTENCE=True` or configure `WebhookKernelSessionManager.enable_persistence=True`. To connect the API, set the environment varible `EG_WEBHOOK_URL` or configure `WebhookKernelSessionManager.webhook_url` to the API endpoint.
28+
29+
### Enabling Authentication
30+
31+
Enabling authentication is an option if the API requries it for requests. Set the environment variable `EG_AUTH_TYPE` or configure `WebhookKernelSessionManager.auth_type` to be either `Basic` or `Digest`. If it is set to an empty string authentication won't be enabled.
32+
33+
Then set the environment variables `EG_WEBHOOK_USERNAME` and `EG_WEBHOOK_PASSWORD` or configure `WebhookKernelSessionManager.webhook_username` and `WebhookKernelSessionManager.webhook_password` to provide the username and password for authentication.
34+
35+
## Testing Kernel Session Persistence
36+
37+
Once kernel session persistence has been enabled and configured, create a kernel by opening up a Jupyter Notebook. Save some variable in that notebook and shutdown Enterprise Gateway using `kill -9 PID`, wher `PID` is the PID of gateway. Restart Enterprise Gateway and refresh you notebook tab. If all worked correctly, the variable should be loaded without the need to rerun the cell.
38+
39+
If you are using docker, ensure the container isn't tied to the PID of Enterprise Gateway. The container should still run after killing that PID.

docs/source/operators/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,5 @@ Jupyter Enterprise Gateway adheres to
6565
config-kernel-override
6666
config-dynamic
6767
config-culling
68+
config-kernel-persistence
6869
config-security

enterprise_gateway/enterprisegatewayapp.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
default_handlers as default_kernelspec_handlers,
3535
)
3636
from .services.sessions.handlers import default_handlers as default_session_handlers
37-
from .services.sessions.kernelsessionmanager import FileKernelSessionManager
37+
from .services.sessions.kernelsessionmanager import (
38+
FileKernelSessionManager,
39+
WebhookKernelSessionManager,
40+
)
3841
from .services.sessions.sessionmanager import SessionManager
3942

4043
try:
@@ -77,7 +80,12 @@ class EnterpriseGatewayApp(EnterpriseGatewayConfigMixin, JupyterApp):
7780
"""
7881

7982
# Also include when generating help options
80-
classes = [KernelSpecCache, FileKernelSessionManager, RemoteMappingKernelManager]
83+
classes = [
84+
KernelSpecCache,
85+
FileKernelSessionManager,
86+
WebhookKernelSessionManager,
87+
RemoteMappingKernelManager,
88+
]
8189

8290
# Enable some command line shortcuts
8391
aliases = aliases

enterprise_gateway/services/sessions/kernelsessionmanager.py

Lines changed: 150 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@
77
import os
88
import threading
99

10+
import requests
1011
from jupyter_core.paths import jupyter_data_dir
11-
from traitlets import Bool, Unicode, default
12+
from requests.auth import HTTPBasicAuth, HTTPDigestAuth
13+
from traitlets import Bool, CaselessStrEnum, Unicode, default
1214
from traitlets.config.configurable import LoggingConfigurable
1315

1416
kernels_lock = threading.Lock()
@@ -385,3 +387,150 @@ def _get_sessions_loc(self):
385387
if not os.path.exists(path):
386388
os.makedirs(path, 0o755)
387389
return path
390+
391+
392+
class WebhookKernelSessionManager(KernelSessionManager):
393+
"""
394+
Performs kernel session persistence operations against URL provided (EG_WEBHOOK_URL). The URL must have 4 endpoints
395+
associated with it. 1 delete endpoint that takes a list of kernel ids in the body, 1 post endpoint that takes kernels id as a
396+
url param and the kernel session as the body, 1 get endpoint that returns all kernel sessions, and 1 get endpoint that returns
397+
a specific kernel session based on kernel id as url param.
398+
"""
399+
400+
# Webhook URL
401+
webhook_url_env = "EG_WEBHOOK_URL"
402+
webhook_url = Unicode(
403+
config=True,
404+
allow_none=True,
405+
help="""URL endpoint for webhook kernel session manager""",
406+
)
407+
408+
@default("webhook_url")
409+
def webhook_url_default(self):
410+
return os.getenv(self.webhook_url_env, None)
411+
412+
# Webhook Username
413+
webhook_username_env = "EG_WEBHOOK_USERNAME"
414+
webhook_username = Unicode(
415+
config=True,
416+
allow_none=True,
417+
help="""Username for webhook kernel session manager API auth""",
418+
)
419+
420+
@default("webhook_username")
421+
def webhook_username_default(self):
422+
return os.getenv(self.webhook_username_env, None)
423+
424+
# Webhook Password
425+
webhook_password_env = "EG_WEBHOOK_PASSWORD"
426+
webhook_password = Unicode(
427+
config=True,
428+
allow_none=True,
429+
help="""Password for webhook kernel session manager API auth""",
430+
)
431+
432+
@default("webhook_password")
433+
def webhook_password_default(self):
434+
return os.getenv(self.webhook_password_env, None)
435+
436+
# Auth Type
437+
auth_type_env = "EG_AUTH_TYPE"
438+
auth_type = CaselessStrEnum(
439+
config=True,
440+
allow_none=True,
441+
values=["basic", "digest"],
442+
help="""Authentication type for webhook kernel session manager API. Either basic, digest or None""",
443+
)
444+
445+
@default("auth_type")
446+
def auth_type_default(self):
447+
return os.getenv(self.auth_type_env, None)
448+
449+
def __init__(self, kernel_manager, **kwargs):
450+
super().__init__(kernel_manager, **kwargs)
451+
if self.enable_persistence:
452+
self.log.info("Webhook kernel session persistence activated")
453+
self.auth = ""
454+
if self.auth_type:
455+
if self.webhook_username and self.webhook_password:
456+
if self.auth_type == "basic":
457+
self.auth = HTTPBasicAuth(self.webhook_username, self.webhook_password)
458+
elif self.auth_type == "digest":
459+
self.auth = HTTPDigestAuth(self.webhook_username, self.webhook_password)
460+
elif self.auth_type is None:
461+
self.auth = ""
462+
else:
463+
self.log.error("No such option for auth_type/EG_AUTH_TYPE")
464+
else:
465+
self.log.error("Username and/or password aren't set")
466+
467+
def delete_sessions(self, kernel_ids):
468+
"""
469+
Deletes kernel sessions from database
470+
471+
:param list of strings kernel_ids: A list of kernel ids
472+
"""
473+
if self.enable_persistence:
474+
response = requests.delete(self.webhook_url, auth=self.auth, json=kernel_ids)
475+
self.log.debug(f"Webhook kernel session deleting: {kernel_ids}")
476+
if response.status_code != 204:
477+
self.log.error(response.raise_for_status())
478+
479+
def save_session(self, kernel_id):
480+
"""
481+
Saves kernel session to database
482+
483+
:param string kernel_id: A kernel id
484+
"""
485+
if self.enable_persistence:
486+
if kernel_id is not None:
487+
temp_session = dict()
488+
temp_session[kernel_id] = self._sessions[kernel_id]
489+
body = KernelSessionManager.pre_save_transformation(temp_session)
490+
response = requests.post(
491+
f"{self.webhook_url}/{kernel_id}", auth=self.auth, json=body
492+
)
493+
self.log.debug(f"Webhook kernel session saving: {kernel_id}")
494+
if response.status_code != 204:
495+
self.log.error(response.raise_for_status())
496+
497+
def load_sessions(self):
498+
"""
499+
Loads kernel sessions from database
500+
"""
501+
if self.enable_persistence:
502+
response = requests.get(self.webhook_url, auth=self.auth)
503+
if response.status_code == 200:
504+
kernel_sessions = response.content
505+
for kernel_session in kernel_sessions:
506+
self._load_session_from_response(kernel_session)
507+
else:
508+
self.log.error(response.raise_for_status())
509+
510+
def load_session(self, kernel_id):
511+
"""
512+
Loads a kernel session from database
513+
514+
:param string kernel_id: A kernel id
515+
"""
516+
if self.enable_persistence:
517+
if kernel_id is not None:
518+
response = requests.get(f"{self.webhook_url}/{kernel_id}", auth=self.auth)
519+
if response.status_code == 200:
520+
kernel_session = response.content
521+
self._load_session_from_response(kernel_session)
522+
else:
523+
self.log.error(response.raise_for_status())
524+
525+
def _load_session_from_response(self, kernel_session: dict):
526+
"""
527+
Loads kernel session to current session
528+
529+
:param dictionary kernel_session: Kernel session information
530+
"""
531+
self.log.debug("Loading saved session(s)")
532+
self._sessions.update(
533+
KernelSessionManager.post_load_transformation(
534+
json.loads(kernel_session)["kernel_session"]
535+
)
536+
)

0 commit comments

Comments
 (0)