progress-page-display-error (#1859)

* display the given when a task fails while on the progress page

* add task info to the error details on progress page and added foreign key from event table to task table w/ burnettk

* do not attempt to add error details if one cannot be found w/ burnettk

* delete pi events when tasks are deleted w/ burnettk

* fixed migration file w/ burnettk

* removed db migration changes w/ burnettk

* pyl w/ burnettk

---------

Co-authored-by: jasquat <jasquat@users.noreply.github.com>
This commit is contained in:
jasquat 2024-07-17 11:53:38 -04:00 committed by GitHub
parent a038d544a9
commit c11e9ba1b4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 66 additions and 6 deletions

View File

@ -10,6 +10,7 @@ from sqlalchemy.orm import validates
from spiffworkflow_backend.helpers.spiff_enum import SpiffEnum
from spiffworkflow_backend.models.db import SpiffworkflowBaseDBModel
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.task import TaskModel # noqa: F401
from spiffworkflow_backend.models.user import UserModel
@ -21,6 +22,7 @@ class ProcessInstanceEventType(SpiffEnum):
process_instance_resumed = "process_instance_resumed"
process_instance_rewound_to_task = "process_instance_rewound_to_task"
process_instance_suspended = "process_instance_suspended"
process_instance_suspended_for_error = "process_instance_suspended_for_error"
process_instance_terminated = "process_instance_terminated"
task_cancelled = "task_cancelled"
task_completed = "task_completed"
@ -36,6 +38,8 @@ class ProcessInstanceEventModel(SpiffworkflowBaseDBModel):
id: int = db.Column(db.Integer, primary_key=True)
# use task guid so we can bulk insert without worrying about whether or not the task has an id yet
# we considered putting a foreign key constraint on this in july 2024, and decided not to mostly
# because it was scary. it would also delete events on reset and migrate, which felt less than ideal.
task_guid: str | None = db.Column(db.String(36), nullable=True, index=True)
process_instance_id: int = db.Column(ForeignKey("process_instance.id"), nullable=False, index=True)
@ -49,6 +53,10 @@ class ProcessInstanceEventModel(SpiffworkflowBaseDBModel):
"ProcessInstanceMigrationDetailModel", back_populates="process_instance_event", cascade="delete"
) # type: ignore
def task(self) -> TaskModel | None:
task_model: TaskModel | None = TaskModel.query.filter_by(guid=self.task_guid).first()
return task_model
@validates("event_type")
def validate_event_type(self, key: str, value: Any) -> Any:
return self.validate_enum_field(key, value, ProcessInstanceEventType)

View File

@ -36,6 +36,8 @@ from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceModelSchema
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_instance import ProcessInstanceTaskDataCannotBeUpdatedError
from spiffworkflow_backend.models.process_instance_error_detail import ProcessInstanceErrorDetailModel
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventModel
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType
from spiffworkflow_backend.models.task import Task
from spiffworkflow_backend.models.task import TaskModel
@ -447,7 +449,7 @@ def task_submit(
def process_instance_progress(
process_instance_id: int,
) -> flask.wrappers.Response:
response: dict[str, Task | ProcessInstanceModel | list] = {}
response: dict[str, Task | ProcessInstanceModel | list | dict[str, str]] = {}
process_instance = _find_process_instance_for_me_or_raise(process_instance_id, include_actions=True)
principal = _find_principal_or_raise()
@ -461,6 +463,34 @@ def process_instance_progress(
# any time we assign this process_instance, the frontend progress page will redirect to process instance show
response["process_instance"] = process_instance
# look for the most recent error event for this instance
if process_instance.status in [ProcessInstanceStatus.error.value, ProcessInstanceStatus.suspended.value]:
pi_error_details = (
ProcessInstanceErrorDetailModel.query.join(
ProcessInstanceEventModel,
ProcessInstanceErrorDetailModel.process_instance_event_id == ProcessInstanceEventModel.id,
)
.filter(
ProcessInstanceEventModel.process_instance_id == process_instance.id,
ProcessInstanceEventModel.event_type.in_( # type: ignore
[
ProcessInstanceEventType.process_instance_error.value,
ProcessInstanceEventType.task_failed.value,
]
),
)
.order_by(ProcessInstanceEventModel.timestamp.desc()) # type: ignore
.first()
)
if pi_error_details is not None:
response["error_details"] = pi_error_details
task_model = pi_error_details.process_instance_event.task()
if task_model is not None:
response["process_instance_event"] = {
"task_definition_identifier": task_model.task_definition.bpmn_identifier,
"task_definition_name": task_model.task_definition.bpmn_name,
}
user_instructions = TaskInstructionsForEndUserModel.retrieve_and_clear(process_instance.id)
response["instructions"] = user_instructions

View File

@ -5,6 +5,8 @@ from spiffworkflow_backend.exceptions.process_entity_not_found_error import Proc
from spiffworkflow_backend.models.db import db
from spiffworkflow_backend.models.process_instance import ProcessInstanceModel
from spiffworkflow_backend.models.process_instance import ProcessInstanceStatus
from spiffworkflow_backend.models.process_instance_event import ProcessInstanceEventType
from spiffworkflow_backend.services.process_instance_tmp_service import ProcessInstanceTmpService
from spiffworkflow_backend.services.process_model_service import ProcessModelService
@ -57,8 +59,9 @@ class ErrorHandlingService:
db.session.commit()
@staticmethod
@classmethod
def _handle_system_notification(
cls,
error: Exception,
process_instance: ProcessInstanceModel,
exception_notification_addresses: list,
@ -94,7 +97,11 @@ class ErrorHandlingService:
db.session.commit()
MessageService.correlate_send_message(message_instance)
@staticmethod
def _set_instance_status(process_instance: ProcessInstanceModel, status: str) -> None:
@classmethod
def _set_instance_status(cls, process_instance: ProcessInstanceModel, status: str) -> None:
process_instance.status = status
db.session.add(process_instance)
if status == ProcessInstanceStatus.suspended.value:
ProcessInstanceTmpService.add_event_to_process_instance(
process_instance, ProcessInstanceEventType.process_instance_suspended_for_error.value, add_to_db_session=True
)

View File

@ -133,7 +133,11 @@ class ProcessInstanceQueueService:
ProcessInstanceTmpService.add_event_to_process_instance(
process_instance, ProcessInstanceEventType.process_instance_error.value, exception=ex
)
ErrorHandlingService.handle_error(process_instance, ex)
# we call dequeued multiple times but we want this code to only happen once.
# assume that if we are not reentering_lock then this is the top level call and should be the one to handle the error.
if not reentering_lock:
ErrorHandlingService.handle_error(process_instance, ex)
raise ex
finally:
if not reentering_lock:

View File

@ -12,7 +12,10 @@ import { HUMAN_TASK_TYPES, refreshAtInterval } from '../helpers';
import HttpService from '../services/HttpService';
import DateAndTimeService from '../services/DateAndTimeService';
import InstructionsForEndUser from './InstructionsForEndUser';
import { ErrorDisplayStateless } from './ErrorDisplay';
import {
ErrorDisplayStateless,
errorForDisplayFromProcessInstanceErrorDetail,
} from './ErrorDisplay';
type OwnProps = {
processInstanceId: number;
@ -60,6 +63,12 @@ export default function ProcessInstanceProgress({
if (result.task && shouldRedirectToTask(result.task)) {
// if task you can complete, go there
navigate(`/tasks/${result.task.process_instance_id}/${result.task.id}`);
} else if (result.error_details && result.process_instance_event) {
const error = errorForDisplayFromProcessInstanceErrorDetail(
result.process_instance_event,
result.error_details,
);
stopRefreshing(error);
} else if (result.process_instance) {
// there is nothing super exciting happening right now. go to process instance.
if (

View File

@ -503,8 +503,10 @@ export interface TaskInstructionForEndUser {
}
export interface ProcessInstanceProgressResponse {
error_details?: ProcessInstanceEventErrorDetail;
instructions: TaskInstructionForEndUser[];
process_instance?: ProcessInstance;
process_instance_event?: ProcessInstanceLogEntry;
task?: ProcessInstanceTask;
}