Skip to content

Commit d6309ba

Browse files
committed
Log (instead of raise) exceptions when running as a server extension
1 parent 926c4d0 commit d6309ba

File tree

5 files changed

+102
-21
lines changed

5 files changed

+102
-21
lines changed

projects/jupyter-server-ydoc/jupyter_server_ydoc/app.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
encode_file_path,
2323
room_id_from_encoded_path,
2424
)
25-
from .websocketserver import JupyterWebsocketServer, RoomNotFound
25+
from .websocketserver import JupyterWebsocketServer, RoomNotFound, exception_logger
2626

2727

2828
class YDocExtension(ExtensionApp):
@@ -107,6 +107,9 @@ def initialize_handlers(self):
107107
rooms_ready=False,
108108
auto_clean_rooms=False,
109109
ystore_class=self.ystore_class,
110+
# Log exceptions, because we don't want the websocket server
111+
# to _ever_ crash permanently in a live jupyter_server.
112+
exception_handler=exception_logger,
110113
log=self.log,
111114
)
112115

projects/jupyter-server-ydoc/jupyter_server_ydoc/handlers.py

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,26 @@ async def prepare(self):
105105
self._document_save_delay,
106106
)
107107

108+
def exception_logger(exception: Exception, log) -> bool:
109+
"""A function that catches any exceptions raised in the websocket
110+
server and logs them.
111+
112+
The protects the websocket server's task group from cancelling
113+
anytime an exception is raised.
114+
"""
115+
room_id = "unknown"
116+
if self.room.room_id:
117+
room_id = self.room.room_id
118+
log.error(
119+
f"Document Room Exception, (room_id={room_id}: ",
120+
exc_info=exception,
121+
)
122+
return True
123+
124+
# Logging exceptions, instead of raising them here to ensure
125+
# that the y-rooms stay alive even after an exception is seen.
126+
self.room.exception_handler = exception_logger
127+
108128
else:
109129
# TransientRoom
110130
# it is a transient document (e.g. awareness)
@@ -203,9 +223,12 @@ async def open(self, room_id):
203223
self.log.error(f"File {file.path} not found.\n{e!r}", exc_info=e)
204224
self.close(1004, f"File {file.path} not found.")
205225
else:
206-
self.log.error(f"Error initializing: {file.path}\n{e!r}", exc_info=e)
226+
self.log.error(
227+
f"Error initializing: {file.path}\n{e!r}", exc_info=e
228+
)
207229
self.close(
208-
1003, f"Error initializing: {file.path}. You need to close the document."
230+
1003,
231+
f"Error initializing: {file.path}. You need to close the document.",
209232
)
210233

211234
# Clean up the room and delete the file loader
@@ -272,16 +295,24 @@ async def on_message(self, message):
272295

273296
user = self.current_user
274297
data = json.dumps(
275-
{"sender": user.username, "timestamp": time.time(), "content": json.loads(msg)}
298+
{
299+
"sender": user.username,
300+
"timestamp": time.time(),
301+
"content": json.loads(msg),
302+
}
276303
).encode("utf8")
277304

278305
for client in self.room.clients:
279306
if client != self:
280307
task = asyncio.create_task(
281-
client.send(bytes([MessageType.CHAT]) + write_var_uint(len(data)) + data)
308+
client.send(
309+
bytes([MessageType.CHAT]) + write_var_uint(len(data)) + data
310+
)
282311
)
283312
self._websocket_server.background_tasks.add(task)
284-
task.add_done_callback(self._websocket_server.background_tasks.discard)
313+
task.add_done_callback(
314+
self._websocket_server.background_tasks.discard
315+
)
285316

286317
self._message_queue.put_nowait(message)
287318
self._websocket_server.ypatch_nb += 1
@@ -300,7 +331,9 @@ def on_close(self) -> None:
300331
if self._room_id != "JupyterLab:globalAwareness":
301332
self._emit_awareness_event(self.current_user.username, "leave")
302333

303-
def _emit(self, level: LogLevel, action: str | None = None, msg: str | None = None) -> None:
334+
def _emit(
335+
self, level: LogLevel, action: str | None = None, msg: str | None = None
336+
) -> None:
304337
_, _, file_id = decode_file_path(self._room_id)
305338
path = self._file_id_manager.get_path(file_id)
306339

@@ -312,12 +345,16 @@ def _emit(self, level: LogLevel, action: str | None = None, msg: str | None = No
312345

313346
self.event_logger.emit(schema_id=JUPYTER_COLLABORATION_EVENTS_URI, data=data)
314347

315-
def _emit_awareness_event(self, username: str, action: str, msg: str | None = None) -> None:
348+
def _emit_awareness_event(
349+
self, username: str, action: str, msg: str | None = None
350+
) -> None:
316351
data = {"roomid": self._room_id, "username": username, "action": action}
317352
if msg:
318353
data["msg"] = msg
319354

320-
self.event_logger.emit(schema_id=JUPYTER_COLLABORATION_AWARENESS_EVENTS_URI, data=data)
355+
self.event_logger.emit(
356+
schema_id=JUPYTER_COLLABORATION_AWARENESS_EVENTS_URI, data=data
357+
)
321358

322359
async def _clean_room(self) -> None:
323360
"""
@@ -387,7 +424,12 @@ async def put(self, path):
387424
# index already exists
388425
self.log.info("Request for Y document '%s' with room ID: %s", path, idx)
389426
data = json.dumps(
390-
{"format": format, "type": content_type, "fileId": idx, "sessionId": SERVER_SESSION}
427+
{
428+
"format": format,
429+
"type": content_type,
430+
"fileId": idx,
431+
"sessionId": SERVER_SESSION,
432+
}
391433
)
392434
self.set_status(200)
393435
return self.finish(data)
@@ -401,7 +443,12 @@ async def put(self, path):
401443
# index successfully created
402444
self.log.info("Request for Y document '%s' with room ID: %s", path, idx)
403445
data = json.dumps(
404-
{"format": format, "type": content_type, "fileId": idx, "sessionId": SERVER_SESSION}
446+
{
447+
"format": format,
448+
"type": content_type,
449+
"fileId": idx,
450+
"sessionId": SERVER_SESSION,
451+
}
405452
)
406453
self.set_status(201)
407454
return self.finish(data)

projects/jupyter-server-ydoc/jupyter_server_ydoc/rooms.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ async def initialize(self) -> None:
123123
if self._document.source != model["content"]:
124124
# TODO: Delete document from the store.
125125
self._emit(
126-
LogLevel.INFO, "initialize", "The file is out-of-sync with the ystore."
126+
LogLevel.INFO,
127+
"initialize",
128+
"The file is out-of-sync with the ystore.",
127129
)
128130
self.log.info(
129131
"Content in file %s is out-of-sync with the ystore %s",
@@ -135,7 +137,9 @@ async def initialize(self) -> None:
135137
if read_from_source:
136138
self._emit(LogLevel.INFO, "load", "Content loaded from disk.")
137139
self.log.info(
138-
"Content in room %s loaded from file %s", self._room_id, self._file.path
140+
"Content in room %s loaded from file %s",
141+
self._room_id,
142+
self._file.path,
139143
)
140144
self._document.source = model["content"]
141145

@@ -146,7 +150,9 @@ async def initialize(self) -> None:
146150
self.ready = True
147151
self._emit(LogLevel.INFO, "initialize", "Room initialized")
148152

149-
def _emit(self, level: LogLevel, action: str | None = None, msg: str | None = None) -> None:
153+
def _emit(
154+
self, level: LogLevel, action: str | None = None, msg: str | None = None
155+
) -> None:
150156
data = {"level": level.value, "room": self._room_id, "path": self._file.path}
151157
if action:
152158
data["action"] = action
@@ -180,8 +186,12 @@ async def _on_outofband_change(self) -> None:
180186
"""
181187
Called when the file got out-of-band changes.
182188
"""
183-
self.log.info("Out-of-band changes. Overwriting the content in room %s", self._room_id)
184-
self._emit(LogLevel.INFO, "overwrite", "Out-of-band changes. Overwriting the room.")
189+
self.log.info(
190+
"Out-of-band changes. Overwriting the content in room %s", self._room_id
191+
)
192+
self._emit(
193+
LogLevel.INFO, "overwrite", "Out-of-band changes. Overwriting the room."
194+
)
185195

186196
try:
187197
model = await self._file.load_content(self._file_format, self._file_type)
@@ -257,9 +267,13 @@ async def _maybe_save_document(self, saving_document: asyncio.Task | None) -> No
257267
return
258268

259269
except OutOfBandChanges:
260-
self.log.info("Out-of-band changes. Overwriting the content in room %s", self._room_id)
270+
self.log.info(
271+
"Out-of-band changes. Overwriting the content in room %s", self._room_id
272+
)
261273
try:
262-
model = await self._file.load_content(self._file_format, self._file_type)
274+
model = await self._file.load_content(
275+
self._file_format, self._file_type
276+
)
263277
except Exception as e:
264278
msg = f"Error loading content from file: {self._file.path}\n{e!r}"
265279
self.log.error(msg, exc_info=e)

projects/jupyter-server-ydoc/jupyter_server_ydoc/websocketserver.py

Lines changed: 19 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,17 @@ 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+
23+
The protects the websocket server's task group from cancelling
24+
anytime an exception is raised.
25+
"""
26+
log.error("Jupyter Websocket Server: ", exc_info=exception)
27+
return True
28+
29+
1930
class JupyterWebsocketServer(WebsocketServer):
2031
"""Ypy websocket server.
2132
@@ -30,9 +41,15 @@ def __init__(
3041
ystore_class: BaseYStore,
3142
rooms_ready: bool = True,
3243
auto_clean_rooms: bool = True,
44+
exception_handler: Callable[[Exception, Logger], bool] | None = None,
3345
log: Logger | None = None,
3446
):
35-
super().__init__(rooms_ready, auto_clean_rooms, log)
47+
super().__init__(
48+
rooms_ready=rooms_ready,
49+
auto_clean_rooms=auto_clean_rooms,
50+
exception_handler=exception_handler,
51+
log=log,
52+
)
3653
self.ystore_class = ystore_class
3754
self.ypatch_nb = 0
3855
self.connected_users: dict[Any, Any] = {}

projects/jupyter-server-ydoc/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ authors = [
3030
dependencies = [
3131
"jupyter_server>=2.4.0,<3.0.0",
3232
"jupyter_ydoc>=2.0.0,<3.0.0",
33-
"pycrdt-websocket>=0.13.0,<0.14.0",
33+
"pycrdt-websocket>=0.13.1,<0.14.0",
3434
"jupyter_events>=0.10.0",
3535
"jupyter_server_fileid>=0.7.0,<1",
3636
"jsonschema>=4.18.0"

0 commit comments

Comments
 (0)