diff --git a/spiffworkflow-backend/poetry.lock b/spiffworkflow-backend/poetry.lock index 4c93cad4b..bb662fe34 100644 --- a/spiffworkflow-backend/poetry.lock +++ b/spiffworkflow-backend/poetry.lock @@ -1889,8 +1889,8 @@ lxml = "*" [package.source] type = "git" url = "https://github.com/sartography/SpiffWorkflow" -reference = "main" -resolved_reference = "1c877dd768053b4cce4c4e14c92caa3216371751" +reference = "bugfix/run-boundary-events-from-engine-steps" +resolved_reference = "067d6a723b2533ba51d83f94f7f3609004474673" [[package]] name = "sqlalchemy" @@ -2273,7 +2273,7 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "1.1" python-versions = ">=3.9,<3.12" -content-hash = "9fea44386fbab29102a051a254058909568c4ee3dbd6a402fb91aacbcf1f7fd2" +content-hash = "3f192bd96668c070edc1270804ba560b7e0df3df09b7f23f2651b2dcd68c1500" [metadata.files] alabaster = [ diff --git a/spiffworkflow-backend/pyproject.toml b/spiffworkflow-backend/pyproject.toml index f182c1939..f3ce57489 100644 --- a/spiffworkflow-backend/pyproject.toml +++ b/spiffworkflow-backend/pyproject.toml @@ -27,7 +27,7 @@ flask-marshmallow = "*" flask-migrate = "*" flask-restful = "*" werkzeug = "*" -SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "main"} +SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "bugfix/run-boundary-events-from-engine-steps"} # SpiffWorkflow = {git = "https://github.com/sartography/SpiffWorkflow", rev = "6cad2981712bb61eca23af1adfafce02d3277cb9"} # SpiffWorkflow = {develop = true, path = "../../SpiffWorkflow" } sentry-sdk = "^1.10" diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py index f44676131..658426c40 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/process_instance_processor.py @@ -1649,7 +1649,7 @@ class ProcessInstanceProcessor: self.save, ) try: - execution_service.do_engine_steps(exit_at, save) + execution_service.run_until_user_input_required_and_save(exit_at, save) finally: # clear out failling spiff tasks here since the ProcessInstanceProcessor creates an instance of the # script engine on a class variable. @@ -1839,7 +1839,7 @@ class ProcessInstanceProcessor: Return either the most recent task data or--if the process instance is complete-- the process data. """ - if self.process_instance_model.status == "complete": + if self.process_instance_model.status == ProcessInstanceStatus.complete.value: return self.get_data() most_recent_task = None diff --git a/spiffworkflow-backend/src/spiffworkflow_backend/services/workflow_execution_service.py b/spiffworkflow-backend/src/spiffworkflow_backend/services/workflow_execution_service.py index e578cc131..d0f73eabd 100644 --- a/spiffworkflow-backend/src/spiffworkflow_backend/services/workflow_execution_service.py +++ b/spiffworkflow-backend/src/spiffworkflow_backend/services/workflow_execution_service.py @@ -192,7 +192,7 @@ class ExecutionStrategy: """__init__.""" self.delegate = delegate - def do_engine_steps(self, bpmn_process_instance: BpmnWorkflow, exit_at: None = None) -> None: + def spiff_run(self, bpmn_process_instance: BpmnWorkflow, exit_at: None = None) -> None: pass def save(self, bpmn_process_instance: BpmnWorkflow) -> None: @@ -202,7 +202,7 @@ class ExecutionStrategy: class GreedyExecutionStrategy(ExecutionStrategy): """The common execution strategy. This will greedily run all engine steps without stopping.""" - def do_engine_steps(self, bpmn_process_instance: BpmnWorkflow, exit_at: None = None) -> None: + def spiff_run(self, bpmn_process_instance: BpmnWorkflow, exit_at: None = None) -> None: self.bpmn_process_instance = bpmn_process_instance bpmn_process_instance.do_engine_steps( exit_at=exit_at, @@ -219,7 +219,7 @@ class RunUntilServiceTaskExecutionStrategy(ExecutionStrategy): return (to an interstitial page). The background processor would then take over. """ - def do_engine_steps(self, bpmn_process_instance: BpmnWorkflow, exit_at: None = None) -> None: + def spiff_run(self, bpmn_process_instance: BpmnWorkflow, exit_at: None = None) -> None: self.bpmn_process_instance = bpmn_process_instance engine_steps = list( [ @@ -278,7 +278,13 @@ class WorkflowExecutionService: self.process_instance_completer = process_instance_completer self.process_instance_saver = process_instance_saver - def do_engine_steps(self, exit_at: None = None, save: bool = False) -> None: + # names of methods that do spiff stuff: + # processor.do_engine_steps calls: + # run_until_user_input_required_and_save + # run_until_user_input_required + # execution_strategy.spiff_run + # spiff.do_engine_steps + def run_until_user_input_required_and_save(self, exit_at: None = None, save: bool = False) -> None: """Do_engine_steps.""" with safe_assertion(ProcessInstanceLockService.has_lock(self.process_instance_model.id)) as tripped: if tripped: @@ -288,16 +294,8 @@ class WorkflowExecutionService: ) try: - self.bpmn_process_instance.refresh_waiting_tasks() + self.run_until_user_input_required(exit_at) - # TODO: implicit re-entrant locks here `with_dequeued` - self.execution_strategy.do_engine_steps(self.bpmn_process_instance, exit_at) - - if self.bpmn_process_instance.is_completed(): - self.process_instance_completer(self.bpmn_process_instance) - - self.process_bpmn_messages() - self.queue_waiting_receive_messages() except SpiffWorkflowException as swe: raise ApiError.from_workflow_exception("task_error", str(swe), swe) from swe @@ -308,6 +306,31 @@ class WorkflowExecutionService: if save: self.process_instance_saver() + def run_until_user_input_required(self, exit_at: None = None) -> None: + """Keeps running tasks until there are no non-human READY tasks. + + spiff.refresh_waiting_tasks is the thing that pushes some waiting tasks to READY. + """ + self.bpmn_process_instance.refresh_waiting_tasks() + + # TODO: implicit re-entrant locks here `with_dequeued` + self.execution_strategy.spiff_run(self.bpmn_process_instance, exit_at) + + if self.bpmn_process_instance.is_completed(): + self.process_instance_completer(self.bpmn_process_instance) + + self.process_bpmn_messages() + self.queue_waiting_receive_messages() + + self.bpmn_process_instance.refresh_waiting_tasks() + ready_tasks = self.bpmn_process_instance.get_tasks(TaskState.READY) + non_human_waiting_task = next( + (p for p in ready_tasks if p.task_spec.spec_type not in ["User Task", "Manual Task"]), None + ) + # non_human_waiting_task = next((p for p in ready_tasks), None) + if non_human_waiting_task is not None: + self.run_until_user_input_required(exit_at) + def process_bpmn_messages(self) -> None: """Process_bpmn_messages.""" bpmn_messages = self.bpmn_process_instance.get_bpmn_messages() @@ -379,11 +402,11 @@ class WorkflowExecutionService: class ProfiledWorkflowExecutionService(WorkflowExecutionService): """A profiled version of the workflow execution service.""" - def do_engine_steps(self, exit_at: None = None, save: bool = False) -> None: + def run_until_user_input_required_and_save(self, exit_at: None = None, save: bool = False) -> None: """__do_engine_steps.""" import cProfile from pstats import SortKey with cProfile.Profile() as pr: - super().do_engine_steps(exit_at=exit_at, save=save) + super().run_until_user_input_required_and_save(exit_at=exit_at, save=save) pr.print_stats(sort=SortKey.CUMULATIVE) diff --git a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model.py b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model.py index 40a9c96ee..1cf7f10d1 100644 --- a/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model.py +++ b/spiffworkflow-backend/tests/spiffworkflow_backend/unit/test_process_model.py @@ -1,4 +1,5 @@ """Process Model.""" +import json import re from flask.app import Flask