Merge pull request #238 from sartography/bug/boudary_event_suspend_manual_complete

boundary event suspend manual complete
This commit is contained in:
Dan Funk 2023-05-04 08:06:31 -04:00 committed by GitHub
commit d769143c83
6 changed files with 231 additions and 33 deletions

View File

@ -18,13 +18,13 @@ def setup_database_uri(app: Flask) -> None:
if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_URI") is None:
database_name = f"spiffworkflow_backend_{app.config['ENV_IDENTIFIER']}"
if app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "sqlite":
app.config["SQLALCHEMY_DATABASE_URI"] = (
f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3"
)
app.config[
"SQLALCHEMY_DATABASE_URI"
] = f"sqlite:///{app.instance_path}/db_{app.config['ENV_IDENTIFIER']}.sqlite3"
elif app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_TYPE") == "postgres":
app.config["SQLALCHEMY_DATABASE_URI"] = (
f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}"
)
app.config[
"SQLALCHEMY_DATABASE_URI"
] = f"postgresql://spiffworkflow_backend:spiffworkflow_backend@localhost:5432/{database_name}"
else:
# use pswd to trick flake8 with hardcoded passwords
db_pswd = app.config.get("SPIFFWORKFLOW_BACKEND_DATABASE_PASSWORD")

View File

@ -129,9 +129,9 @@ class ProcessInstanceModel(SpiffworkflowBaseDBModel):
def serialized_with_metadata(self) -> dict[str, Any]:
process_instance_attributes = self.serialized
process_instance_attributes["process_metadata"] = self.process_metadata
process_instance_attributes["process_model_with_diagram_identifier"] = (
self.process_model_with_diagram_identifier
)
process_instance_attributes[
"process_model_with_diagram_identifier"
] = self.process_model_with_diagram_identifier
return process_instance_attributes
@property

View File

@ -423,9 +423,9 @@ class ProcessInstanceProcessor:
tld.process_instance_id = process_instance_model.id
# we want this to be the fully qualified path to the process model including all group subcomponents
current_app.config["THREAD_LOCAL_DATA"].process_model_identifier = (
f"{process_instance_model.process_model_identifier}"
)
current_app.config[
"THREAD_LOCAL_DATA"
].process_model_identifier = f"{process_instance_model.process_model_identifier}"
self.process_instance_model = process_instance_model
self.process_model_service = ProcessModelService()
@ -585,9 +585,9 @@ class ProcessInstanceProcessor:
bpmn_subprocess_definition.bpmn_identifier
] = bpmn_process_definition_dict
spiff_bpmn_process_dict["subprocess_specs"][bpmn_subprocess_definition.bpmn_identifier]["task_specs"] = {}
bpmn_subprocess_definition_bpmn_identifiers[bpmn_subprocess_definition.id] = (
bpmn_subprocess_definition.bpmn_identifier
)
bpmn_subprocess_definition_bpmn_identifiers[
bpmn_subprocess_definition.id
] = bpmn_subprocess_definition.bpmn_identifier
task_definitions = TaskDefinitionModel.query.filter(
TaskDefinitionModel.bpmn_process_definition_id.in_( # type: ignore
@ -1115,59 +1115,54 @@ class ProcessInstanceProcessor:
def manual_complete_task(self, task_id: str, execute: bool) -> None:
"""Mark the task complete optionally executing it."""
spiff_tasks_updated = {}
start_in_seconds = time.time()
spiff_task = self.bpmn_process_instance.get_task_from_id(UUID(task_id))
event_type = ProcessInstanceEventType.task_skipped.value
start_time = time.time()
if execute:
current_app.logger.info(
f"Manually executing Task {spiff_task.task_spec.name} of process"
f" instance {self.process_instance_model.id}"
)
# Executing a subworkflow manually will restart its subprocess and allow stepping through it
# Executing a sub-workflow manually will restart its subprocess and allow stepping through it
if isinstance(spiff_task.task_spec, SubWorkflowTask):
subprocess = self.bpmn_process_instance.get_subprocess(spiff_task)
# We have to get to the actual start event
for task in self.bpmn_process_instance.get_tasks(workflow=subprocess):
task.complete()
spiff_tasks_updated[task.id] = task
if isinstance(task.task_spec, StartEvent):
for spiff_task in self.bpmn_process_instance.get_tasks(workflow=subprocess):
spiff_task.run()
if isinstance(spiff_task.task_spec, StartEvent):
break
else:
spiff_task.complete()
spiff_tasks_updated[spiff_task.id] = spiff_task
for child in spiff_task.children:
spiff_tasks_updated[child.id] = child
spiff_task.run()
event_type = ProcessInstanceEventType.task_executed_manually.value
else:
spiff_logger = logging.getLogger("spiff")
spiff_logger.info(f"Skipped task {spiff_task.task_spec.name}", extra=spiff_task.log_info())
spiff_task._set_state(TaskState.COMPLETED)
for child in spiff_task.children:
child.task_spec._update(child)
spiff_tasks_updated[child.id] = child
spiff_task.complete()
spiff_task.workflow.last_task = spiff_task
spiff_tasks_updated[spiff_task.id] = spiff_task
end_in_seconds = time.time()
if isinstance(spiff_task.task_spec, EndEvent):
for task in self.bpmn_process_instance.get_tasks(TaskState.DEFINITE_MASK, workflow=spiff_task.workflow):
task.complete()
spiff_tasks_updated[task.id] = task
# A subworkflow task will become ready when its workflow is complete. Engine steps would normally
# then complete it, but we have to do it ourselves here.
for task in self.bpmn_process_instance.get_tasks(TaskState.READY):
if isinstance(task.task_spec, SubWorkflowTask):
task.complete()
spiff_tasks_updated[task.id] = task
task_service = TaskService(
process_instance=self.process_instance_model,
serializer=self._serializer,
bpmn_definition_to_task_definitions_mappings=self.bpmn_definition_to_task_definitions_mappings,
)
spiff_tasks_updated = {}
for task in self.bpmn_process_instance.get_tasks():
if task.last_state_change > start_time:
spiff_tasks_updated[task.id] = task
for updated_spiff_task in spiff_tasks_updated.values():
(
bpmn_process,
@ -1216,6 +1211,14 @@ class ProcessInstanceProcessor:
raise TaskNotFoundError(
f"Cannot find a task with guid '{to_task_guid}' for process instance '{process_instance.id}'"
)
# If this task model has a parent boundary event, reset to that point instead,
# so we can reset all the boundary timers, etc...
parent_id = to_task_model.properties_json.get("parent", "")
parent = TaskModel.query.filter_by(guid=parent_id).first()
is_boundary_parent = False
if parent and parent.task_definition.typename == "_BoundaryEventParent":
to_task_model = parent
is_boundary_parent = True # Will need to complete this task at the end so we are on the correct process.
# NOTE: run ALL queries before making changes to ensure we get everything before anything changes
parent_bpmn_processes, task_models_of_parent_bpmn_processes = TaskService.task_models_of_parent_bpmn_processes(
@ -1320,6 +1323,11 @@ class ProcessInstanceProcessor:
db.session.commit()
processor = ProcessInstanceProcessor(process_instance)
# If this as a boundary event parent, run it, so we get back to an active task.
if is_boundary_parent:
processor.do_engine_steps(execution_strategy_name="one_at_a_time")
processor.save()
processor.suspend()

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="Process_Admin_Tools_Test" name="AdminToolsTest" isExecutable="true">
<bpmn:startEvent id="Event_17e2qgy">
<bpmn:outgoing>Flow_1ist4rn</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1ist4rn" sourceRef="Event_17e2qgy" targetRef="Activity_039a4i7" />
<bpmn:endEvent id="Event_1qodpuj">
<bpmn:incoming>Flow_1xbry1g</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_0vzi07z" sourceRef="Activity_039a4i7" targetRef="Activity_0sqxs4d" />
<bpmn:callActivity id="Activity_039a4i7" calledElement="Process_With_Timer">
<bpmn:incoming>Flow_1ist4rn</bpmn:incoming>
<bpmn:outgoing>Flow_0vzi07z</bpmn:outgoing>
</bpmn:callActivity>
<bpmn:sequenceFlow id="Flow_1xbry1g" sourceRef="Activity_0sqxs4d" targetRef="Event_1qodpuj" />
<bpmn:manualTask id="Activity_0sqxs4d" name="Final">
<bpmn:incoming>Flow_0vzi07z</bpmn:incoming>
<bpmn:outgoing>Flow_1xbry1g</bpmn:outgoing>
</bpmn:manualTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_Admin_Tools_Test">
<bpmndi:BPMNShape id="Event_17e2qgy_di" bpmnElement="Event_17e2qgy">
<dc:Bounds x="352" y="152" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_02srymo_di" bpmnElement="Activity_039a4i7">
<dc:Bounds x="440" y="130" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1qodpuj_di" bpmnElement="Event_1qodpuj">
<dc:Bounds x="742" y="152" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1r3vbnd_di" bpmnElement="Activity_0sqxs4d">
<dc:Bounds x="600" y="130" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1ist4rn_di" bpmnElement="Flow_1ist4rn">
<di:waypoint x="388" y="170" />
<di:waypoint x="440" y="170" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vzi07z_di" bpmnElement="Flow_0vzi07z">
<di:waypoint x="540" y="170" />
<di:waypoint x="600" y="170" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1xbry1g_di" bpmnElement="Flow_1xbry1g">
<di:waypoint x="700" y="170" />
<di:waypoint x="742" y="170" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" id="Definitions_96f6665" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="3.0.0-dev">
<bpmn:process id="Process_With_Timer" name="Process With Timer" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_1e5apvr</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_1e5apvr" sourceRef="StartEvent_1" targetRef="manual_task_1" />
<bpmn:sequenceFlow id="Flow_0vtgres" sourceRef="manual_task_1" targetRef="Activity_2" />
<bpmn:endEvent id="Event_1pgaya7">
<bpmn:incoming>Flow_110vf76</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_110vf76" sourceRef="Activity_2" targetRef="Event_1pgaya7" />
<bpmn:boundaryEvent id="Timer_Event_Horror" name="Timer_Event_Horror" attachedToRef="manual_task_1">
<bpmn:outgoing>Flow_1hy0t7d</bpmn:outgoing>
<bpmn:timerEventDefinition id="TimerEventDefinition_1jkwn61">
<bpmn:timeDuration xsi:type="bpmn:tFormalExpression">'P14D'</bpmn:timeDuration>
</bpmn:timerEventDefinition>
</bpmn:boundaryEvent>
<bpmn:sequenceFlow id="Flow_1hy0t7d" sourceRef="Timer_Event_Horror" targetRef="Activity_3" />
<bpmn:endEvent id="Event_10frcbe">
<bpmn:incoming>Flow_1xbdri7</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_1xbdri7" sourceRef="Activity_3" targetRef="Event_10frcbe" />
<bpmn:manualTask id="manual_task_1" name="Manual Task #1">
<bpmn:incoming>Flow_1e5apvr</bpmn:incoming>
<bpmn:outgoing>Flow_0vtgres</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:manualTask id="Activity_3" name="#3">
<bpmn:incoming>Flow_1hy0t7d</bpmn:incoming>
<bpmn:outgoing>Flow_1xbdri7</bpmn:outgoing>
</bpmn:manualTask>
<bpmn:scriptTask id="Activity_2" name="#2">
<bpmn:incoming>Flow_0vtgres</bpmn:incoming>
<bpmn:outgoing>Flow_110vf76</bpmn:outgoing>
<bpmn:script>y='1000'</bpmn:script>
</bpmn:scriptTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_With_Timer">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1pgaya7_di" bpmnElement="Event_1pgaya7">
<dc:Bounds x="592" y="159" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_10frcbe_di" bpmnElement="Event_10frcbe">
<dc:Bounds x="592" y="282" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1cl74h9_di" bpmnElement="manual_task_1">
<dc:Bounds x="270" y="137" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_0yn987b_di" bpmnElement="Activity_3">
<dc:Bounds x="430" y="260" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_19c5vp3_di" bpmnElement="Activity_2">
<dc:Bounds x="430" y="137" width="100" height="80" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_1gfn4de_di" bpmnElement="Timer_Event_Horror">
<dc:Bounds x="302" y="199" width="36" height="36" />
<bpmndi:BPMNLabel>
<dc:Bounds x="277" y="242" width="87" height="27" />
</bpmndi:BPMNLabel>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_1e5apvr_di" bpmnElement="Flow_1e5apvr">
<di:waypoint x="215" y="177" />
<di:waypoint x="270" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0vtgres_di" bpmnElement="Flow_0vtgres">
<di:waypoint x="370" y="177" />
<di:waypoint x="430" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_110vf76_di" bpmnElement="Flow_110vf76">
<di:waypoint x="530" y="177" />
<di:waypoint x="592" y="177" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1hy0t7d_di" bpmnElement="Flow_1hy0t7d">
<di:waypoint x="320" y="235" />
<di:waypoint x="320" y="300" />
<di:waypoint x="430" y="300" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_1xbdri7_di" bpmnElement="Flow_1xbdri7">
<di:waypoint x="530" y="300" />
<di:waypoint x="592" y="300" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>

View File

@ -434,6 +434,56 @@ class TestProcessInstanceProcessor(BaseTest):
assert process_instance.status == "complete"
def test_properly_resets_process_on_tasks_with_boundary_events(
self,
app: Flask,
client: FlaskClient,
with_db_and_bpmn_file_cleanup: None,
with_super_admin_user: UserModel,
) -> None:
self.create_process_group_with_api(client, with_super_admin_user, "test_group", "test_group")
process_model = load_test_spec(
process_model_id="test_group/boundary_event_reset",
process_model_source_directory="boundary_event_reset",
)
process_instance = self.create_process_instance_from_process_model(
process_model=process_model, user=with_super_admin_user
)
processor = ProcessInstanceProcessor(process_instance)
processor.do_engine_steps(save=True)
assert len(process_instance.active_human_tasks) == 1
human_task_one = process_instance.active_human_tasks[0]
spiff_manual_task = processor.bpmn_process_instance.get_task_from_id(UUID(human_task_one.task_id))
ProcessInstanceService.complete_form_task(
processor, spiff_manual_task, {}, with_super_admin_user, human_task_one
)
assert (
len(process_instance.active_human_tasks) == 1
), "expected 1 active human tasks after 2nd one is completed"
assert process_instance.active_human_tasks[0].task_title == "Final"
# Reset the process back to the task within the call activity that contains a timer_boundary event.
reset_to_spiff_task: SpiffTask = processor.__class__.get_task_by_bpmn_identifier(
"manual_task_1", processor.bpmn_process_instance
)
processor.suspend()
processor = ProcessInstanceProcessor(process_instance)
ProcessInstanceProcessor.reset_process(process_instance, str(reset_to_spiff_task.id))
human_task_one = process_instance.active_human_tasks[0]
assert human_task_one.task_title == "Manual Task #1"
processor = ProcessInstanceProcessor(process_instance)
processor.manual_complete_task(str(spiff_manual_task.id), execute=True)
processor = ProcessInstanceProcessor(process_instance)
processor.resume()
processor.do_engine_steps(save=True)
process_instance = ProcessInstanceModel.query.filter_by(id=process_instance.id).first()
assert len(process_instance.active_human_tasks) == 1
assert process_instance.active_human_tasks[0].task_title == "Final", (
"once we reset, resume, and complete the task, we should be back to the Final step again, and not"
"stuck waiting for the call activity to complete (which was happening in a bug I'm fixing right now)"
)
def test_properly_saves_tasks_when_running(
self,
app: Flask,