Skip to content

Commit b6a7309

Browse files
authored
Backport 'Log (instead of raise) exceptions when running as a server extension' #295 (#297)
1 parent 2851ddf commit b6a7309

File tree

5 files changed

+55
-10
lines changed

5 files changed

+55
-10
lines changed

jupyter_collaboration/app.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from .loaders import FileLoaderMapping
1313
from .stores import SQLiteYStore
1414
from .utils import AWARENESS_EVENTS_SCHEMA_PATH, EVENTS_SCHEMA_PATH
15-
from .websocketserver import JupyterWebsocketServer
15+
from .websocketserver import JupyterWebsocketServer, exception_logger
1616

1717

1818
class YDocExtension(ExtensionApp):
@@ -85,6 +85,9 @@ def initialize_handlers(self):
8585
rooms_ready=False,
8686
auto_clean_rooms=False,
8787
ystore_class=self.ystore_class,
88+
# Log exceptions, because we don't want the websocket server
89+
# to _ever_ crash permanently in a live jupyter_server.
90+
exception_handler=exception_logger,
8891
log=self.log,
8992
)
9093

jupyter_collaboration/handlers.py

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import json
88
import time
99
import uuid
10+
from logging import Logger
1011
from typing import Any
1112

1213
from jupyter_server.auth import authorized
@@ -83,6 +84,20 @@ async def prepare(self):
8384
if self._websocket_server.room_exists(self._room_id):
8485
self.room: YRoom = await self._websocket_server.get_room(self._room_id)
8586
else:
87+
# Logging exceptions, instead of raising them here to ensure
88+
# that the y-rooms stay alive even after an exception is seen.
89+
def exception_logger(exception: Exception, log: Logger) -> bool:
90+
"""A function that catches any exceptions raised in the websocket
91+
server and logs them.
92+
The protects the y-room's task group from cancelling
93+
anytime an exception is raised.
94+
"""
95+
log.error(
96+
f"Document Room Exception, (room_id={self._room_id or 'unknown'}): ",
97+
exc_info=exception,
98+
)
99+
return True
100+
86101
if self._room_id.count(":") >= 2:
87102
# DocumentRoom
88103
file_format, file_type, file_id = decode_file_path(self._room_id)
@@ -104,13 +119,18 @@ async def prepare(self):
104119
self.event_logger,
105120
ystore,
106121
self.log,
107-
self._document_save_delay,
122+
exception_handler=exception_logger,
123+
save_delay=self._document_save_delay,
108124
)
109125

110126
else:
111127
# TransientRoom
112128
# it is a transient document (e.g. awareness)
113-
self.room = TransientRoom(self._room_id, self.log)
129+
self.room = TransientRoom(
130+
self._room_id,
131+
log=self.log,
132+
exception_handler=exception_logger,
133+
)
114134

115135
try:
116136
await self._websocket_server.start_room(self.room)

jupyter_collaboration/rooms.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import asyncio
77
from logging import Logger
8-
from typing import Any
8+
from typing import Any, Callable
99

1010
from jupyter_events import EventLogger
1111
from jupyter_ydoc import ydocs as YDOCS
@@ -31,8 +31,9 @@ def __init__(
3131
ystore: BaseYStore | None,
3232
log: Logger | None,
3333
save_delay: float | None = None,
34+
exception_handler: Callable[[Exception, Logger], bool] | None = None,
3435
):
35-
super().__init__(ready=False, ystore=ystore, log=log)
36+
super().__init__(ready=False, ystore=ystore, exception_handler=exception_handler, log=log)
3637

3738
self._room_id: str = room_id
3839
self._file_format: str = file_format
@@ -284,8 +285,13 @@ async def _maybe_save_document(self, saving_document: asyncio.Task | None) -> No
284285
class TransientRoom(YRoom):
285286
"""A Y room for sharing state (e.g. awareness)."""
286287

287-
def __init__(self, room_id: str, log: Logger | None):
288-
super().__init__(log=log)
288+
def __init__(
289+
self,
290+
room_id: str,
291+
log: Logger | None = None,
292+
exception_handler: Callable[[Exception, Logger], bool] | None = None,
293+
):
294+
super().__init__(log=log, exception_handler=exception_handler)
289295

290296
self._room_id = room_id
291297

jupyter_collaboration/websocketserver.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import asyncio
77
from logging import Logger
8-
from typing import Any
8+
from typing import Any, Callable
99

1010
from pycrdt_websocket.websocket_server import WebsocketServer, YRoom
1111
from pycrdt_websocket.ystore import BaseYStore
@@ -16,6 +16,16 @@ class RoomNotFound(LookupError):
1616
pass
1717

1818

19+
def exception_logger(exception: Exception, log: Logger) -> bool:
20+
"""A function that catches any exceptions raised in the websocket
21+
server and logs them.
22+
This protects the websocket server's task group from cancelling
23+
anytime an exception is raised.
24+
"""
25+
log.error("Jupyter Websocket Server: ", exc_info=exception)
26+
return True
27+
28+
1929
class JupyterWebsocketServer(WebsocketServer):
2030
"""Ypy websocket server.
2131
@@ -30,9 +40,15 @@ def __init__(
3040
ystore_class: BaseYStore,
3141
rooms_ready: bool = True,
3242
auto_clean_rooms: bool = True,
43+
exception_handler: Callable[[Exception, Logger], bool] | None = None,
3344
log: Logger | None = None,
3445
):
35-
super().__init__(rooms_ready, auto_clean_rooms, log)
46+
super().__init__(
47+
rooms_ready=rooms_ready,
48+
auto_clean_rooms=auto_clean_rooms,
49+
exception_handler=exception_handler,
50+
log=log,
51+
)
3652
self.ystore_class = ystore_class
3753
self.ypatch_nb = 0
3854
self.connected_users: dict[Any, Any] = {}

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ classifiers = [
2929
dependencies = [
3030
"jupyter_server>=2.0.0,<3.0.0",
3131
"jupyter_ydoc>=2.0.0,<3.0.0",
32-
"pycrdt-websocket>=0.13.0,<0.14.0",
32+
"pycrdt-websocket>=0.13.1,<0.14.0",
3333
"jupyter_events>=0.10.0",
3434
"jupyter_server_fileid>=0.7.0,<1",
3535
"jsonschema>=4.18.0"

0 commit comments

Comments
 (0)