Skip to content

Commit

Permalink
Report exceptions in notebook execution in "error"
Browse files Browse the repository at this point in the history
This modifies the result payload for a notebook execution job when the
job is complete.

- Add a new "error" field that reports an exception raised during the
  noteburst job, which explains why a job's "success" field is false.
  The field contains a NoteburstExecutionError model, which in turn
  contains an enum code for the error and a text message describing the
  error.

- Keep the existing "ipynb_error" field for reporting exceptions raised
  within the notebook itself. Noteburst's execution could still be
  considered successful even if the notebook itself raised an exception.
  • Loading branch information
jonathansick committed Sep 10, 2024
1 parent 0a5a0b0 commit 2019f23
Showing 1 changed file with 79 additions and 4 deletions.
83 changes: 79 additions & 4 deletions src/noteburst/handlers/v1/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import json
from datetime import datetime, timedelta
from enum import Enum
from typing import Annotated, Any

from arq.jobs import JobStatus
Expand All @@ -12,6 +13,7 @@
from safir.arq import JobMetadata, JobResult
from safir.pydantic import HumanTimedelta

from noteburst.exceptions import NbexecTaskError, NbexecTaskTimeoutError
from noteburst.jupyterclient.jupyterlab import (
NotebookExecutionErrorModel,
NotebookExecutionResult,
Expand Down Expand Up @@ -48,6 +50,34 @@ def from_nbexec_error(
)


class NoteburstErrorCodes(Enum):
"""Error codes for Noteburst errors."""

timeout = "timeout"
"""The notebook execution timed out."""

jupyter_error = "jupyter_error"
"""An error occurred contacting the Jupyter server."""

unknown = "unknown"
"""An unknown error occurred."""


class NoteburstExecutionError(BaseModel):
"""Information about an exception that occurred during noteburst's
execution of a notebook (other than an exception raised in the notebook
itself).
"""

code: NoteburstErrorCodes = Field(
description="The reference code of the error."
)

message: str | None = Field(
None, description="Additional information about the exception."
)


class NotebookResponse(BaseModel):
"""Information about a notebook execution job, possibly including the
result and source notebooks.
Expand Down Expand Up @@ -102,6 +132,17 @@ class NotebookResponse(BaseModel):
),
] = None

error: Annotated[
NoteburstExecutionError | None,
Field(
description=(
"An error occurred during notebook execution, other than an "
"exception in the notebook itself. This field is null if an "
"error did not occur."
)
),
] = None

ipynb: Annotated[
str | None,
Field(
Expand Down Expand Up @@ -130,18 +171,51 @@ async def from_job_metadata(
job_result: JobResult | None = None,
) -> NotebookResponse:
"""Create a NotebookResponse from a job."""
# When a job is a "success" it means that the arq worker didn't raise
# an exception, so we can expect an ipynb result. However the ipynb
# might have still raised an exception which is part of
# nbexec_result.error and we want to pass that back to the user.
if job_result is not None and job_result.success:
nbexec_result = NotebookExecutionResult.model_validate_json(
job_result.result
)
ipynb = nbexec_result.notebook
if nbexec_result.error:
error = NotebookError.from_nbexec_error(nbexec_result.error)
ipynb_error = NotebookError.from_nbexec_error(
nbexec_result.error
)
else:
error = None
ipynb_error = None
else:
ipynb = None
error = None
ipynb_error = None

# In this case the job is complete but failed (an exception was raised)
# so we want to pass the exception back to the user.
if job_result and not job_result.success:
try:
# Attempt to access the result to raise any exceptions
job_result.result # noqa: B018
except NbexecTaskTimeoutError as e:
# format this error
noteburst_error = NoteburstExecutionError(
code=NoteburstErrorCodes.timeout,
message=str(e),
)
except NbexecTaskError as e:
# format this error
noteburst_error = NoteburstExecutionError(
code=NoteburstErrorCodes.jupyter_error,
message=str(e),
)
except Exception as e:
# format an unknown error
noteburst_error = NoteburstExecutionError(
code=NoteburstErrorCodes.unknown,
message=str(e),
)
else:
noteburst_error = None

return cls(
job_id=job.id,
Expand All @@ -153,8 +227,9 @@ async def from_job_metadata(
start_time=job_result.start_time if job_result else None,
finish_time=job_result.finish_time if job_result else None,
success=job_result.success if job_result else None,
error=noteburst_error,
ipynb=ipynb,
ipynb_error=error,
ipynb_error=ipynb_error,
)


Expand Down

0 comments on commit 2019f23

Please sign in to comment.