|
1 | 1 | """The WorkflowEngine execution logic.
|
2 | 2 | """
|
3 | 3 |
|
| 4 | +import ast |
4 | 5 | import logging
|
5 | 6 | import sys
|
| 7 | +from typing import Any, Dict |
6 | 8 |
|
7 | 9 | from google.protobuf.message import Message
|
8 | 10 | from informaticsmatters.protobuf.datamanager.pod_message_pb2 import PodMessage
|
@@ -54,22 +56,151 @@ def handle_message(self, msg: Message) -> None:
|
54 | 56 | self._handle_workflow_message(msg)
|
55 | 57 |
|
56 | 58 | def _handle_workflow_message(self, msg: WorkflowMessage) -> None:
|
| 59 | + """Handles a WorkflowMessage. This is a message that signals a START or STOP |
| 60 | + of a workflow. On START we will load the workflow definition and run (launch) |
| 61 | + the first step.""" |
57 | 62 | assert msg
|
58 | 63 |
|
| 64 | + # ALL THIS CODE ADDED SIMPLY TO DEMONSTRATE THE USE OF THE API ADAPTER |
| 65 | + # AND THE INSTANCE LAUNCHER FOR THE SIMPLEST OF WORKFLOWS: - |
| 66 | + # THE "TWO-STEP NOP". |
| 67 | + # THERE IS NWO SPECIFICATION MANIPULATION NEEDED FOR THIS EXAMPLE |
| 68 | + # THE STEPS HAVE NO INPUTS OR OUTPUTS. |
| 69 | + # THIS FUNCTION PROBABLY NEEDS TO BE A LOT MORE SOPHISTICATED! |
| 70 | + |
59 | 71 | _LOGGER.info("WE> WorkflowMessage:\n%s", str(msg))
|
60 | 72 | if msg.action == "START":
|
61 |
| - _LOGGER.info("action=%s", msg.action) |
62 |
| - # Load the workflow definition and run the first step... |
63 |
| - rf = self._api_adapter.get_running_workflow( |
| 73 | + # Using the running workflow get the workflow definition |
| 74 | + response = self._api_adapter.get_running_workflow( |
64 | 75 | running_workflow_id=msg.running_workflow
|
65 | 76 | )
|
66 |
| - assert rf |
67 |
| - _LOGGER.info("RunningWorkflow: %s", rf) |
| 77 | + assert "running_workflow" in response |
| 78 | + running_workflow = response["running_workflow"] |
| 79 | + _LOGGER.info("RunningWorkflow: %s", running_workflow) |
| 80 | + workflow_id = running_workflow["workflow"]["id"] |
| 81 | + response = self._api_adapter.get_workflow(workflow_id=workflow_id) |
| 82 | + assert "workflow" in response |
| 83 | + workflow = response["workflow"] |
| 84 | + # Now find the first step |
| 85 | + # and create a RunningWorkflowStep record prior to launching the instance |
| 86 | + response = self._api_adapter.create_running_workflow_step( |
| 87 | + running_workflow_id=msg.running_workflow, |
| 88 | + step=workflow["steps"][0]["name"], |
| 89 | + ) |
| 90 | + assert "id" in response |
| 91 | + running_workflow_step_id = response["id"] |
| 92 | + # The specification is a string here. |
| 93 | + # It needs to be a dictionary for the launch() method. |
| 94 | + step = workflow["steps"][0] |
| 95 | + step_specification: Dict[str, Any] = ast.literal_eval(step["specification"]) |
| 96 | + self._instance_launcher.launch( |
| 97 | + project_id="project-000", |
| 98 | + workflow_id=workflow_id, |
| 99 | + running_workflow_step_id=running_workflow_step_id, |
| 100 | + workflow_definition=workflow, |
| 101 | + step_specification=step_specification, |
| 102 | + ) |
68 | 103 |
|
69 | 104 | else:
|
70 | 105 | _LOGGER.info("action=%s", msg.action)
|
71 | 106 |
|
72 | 107 | def _handle_pod_message(self, msg: PodMessage) -> None:
|
| 108 | + """Handles a PodMessage. This is a message that signals the completion of a |
| 109 | + step within a workflow. Steps run as "instances" and the Pod message |
| 110 | + identifies the Instance. Using the Instance record we can get the |
| 111 | + "running workflow step" and then identify the "running workflow" and the |
| 112 | + "workflow". |
| 113 | +
|
| 114 | + First thing is to adjust the workflow step with the step's success state and |
| 115 | + optional error code. If the step was successful we can find the next step |
| 116 | + and launch that, or consider the last step to have run and modify the |
| 117 | + running workflow record and set's it's success status.""" |
73 | 118 | assert msg
|
74 | 119 |
|
| 120 | + # The PodMessage has a 'instance', 'has_exit_code', and 'exit_code' values. |
75 | 121 | _LOGGER.info("WE> PodMessage:\n%s", str(msg))
|
| 122 | + |
| 123 | + # ALL THIS CODE ADDED SIMPLY TO DEMONSTRATE THE USE OF THE API ADAPTER |
| 124 | + # AND THE INSTANCE LAUNCHER FOR THE SIMPLEST OF WORKFLOWS: - |
| 125 | + # THE "TWO-STEP NOP". |
| 126 | + # THERE IS NWO SPECIFICATION MANIPULATION NEEDED FOR THIS EXAMPLE |
| 127 | + # THE STEPS HAVE NO INPUTS OR OUTPUTS. |
| 128 | + # THIS FUNCTION PROBABLY NEEDS TO BE A LOT MORE SOPHISTICATED! |
| 129 | + |
| 130 | + # Ignore anything without an exit code. |
| 131 | + if not msg.has_exit_code: |
| 132 | + _LOGGER.warning("WE> PodMessage: No exit code") |
| 133 | + return |
| 134 | + |
| 135 | + instance_id: str = msg.instance |
| 136 | + exit_code: int = msg.exit_code |
| 137 | + _LOGGER.info( |
| 138 | + "WE> PodMessage: instance=%s, exit_code=%d", instance_id, exit_code |
| 139 | + ) |
| 140 | + |
| 141 | + # Ignore instances without a running workflow step |
| 142 | + response = self._api_adapter.get_instance(instance_id=instance_id) |
| 143 | + if "running_workflow_step" not in response: |
| 144 | + _LOGGER.warning("WE> PodMessage: Without running_workflow_step") |
| 145 | + return |
| 146 | + running_workflow_step_id: str = response["running_workflow_step"] |
| 147 | + response = self._api_adapter.get_running_workflow_step( |
| 148 | + running_workflow_step_id=running_workflow_step_id |
| 149 | + ) |
| 150 | + step_name: str = response["running_workflow_step"]["step"] |
| 151 | + |
| 152 | + # Set the step as completed (successful or otherwise) |
| 153 | + success: bool = exit_code == 0 |
| 154 | + self._api_adapter.set_running_workflow_step_done( |
| 155 | + running_workflow_step_id=running_workflow_step_id, |
| 156 | + success=success, |
| 157 | + ) |
| 158 | + |
| 159 | + # Get the step's running workflow and workflow IDs and records. |
| 160 | + running_workflow_id = response["running_workflow_step"]["running_workflow"] |
| 161 | + assert running_workflow_id |
| 162 | + response = self._api_adapter.get_running_workflow( |
| 163 | + running_workflow_id=running_workflow_id |
| 164 | + ) |
| 165 | + workflow_id = response["running_workflow"]["workflow"]["id"] |
| 166 | + assert workflow_id |
| 167 | + response = self._api_adapter.get_workflow(workflow_id=workflow_id) |
| 168 | + workflow = response["workflow"] |
| 169 | + |
| 170 | + end_of_workflow: bool = False |
| 171 | + if success: |
| 172 | + # Given the step for the instance just finished, |
| 173 | + # find the next step in the workflow and launch it. |
| 174 | + # If there are no more steps then the workflow is done |
| 175 | + # so we need to set the running workflow as done |
| 176 | + # and set it's success status too. |
| 177 | + for step in workflow["steps"]: |
| 178 | + if step["name"] == step_name: |
| 179 | + step_index = workflow["steps"].index(step) |
| 180 | + if step_index + 1 < len(workflow["steps"]): |
| 181 | + next_step = workflow["steps"][step_index + 1] |
| 182 | + response = self._api_adapter.create_running_workflow_step( |
| 183 | + running_workflow_id=running_workflow_id, |
| 184 | + step=next_step["name"], |
| 185 | + ) |
| 186 | + assert "id" in response |
| 187 | + running_workflow_step_id = response["id"] |
| 188 | + step_specification: Dict[str, Any] = ast.literal_eval( |
| 189 | + next_step["specification"] |
| 190 | + ) |
| 191 | + self._instance_launcher.launch( |
| 192 | + project_id="project-000", |
| 193 | + workflow_id=workflow_id, |
| 194 | + running_workflow_step_id=running_workflow_step_id, |
| 195 | + workflow_definition=workflow, |
| 196 | + step_specification=step_specification, |
| 197 | + ) |
| 198 | + break |
| 199 | + else: |
| 200 | + end_of_workflow = True |
| 201 | + |
| 202 | + if end_of_workflow: |
| 203 | + self._api_adapter.set_running_workflow_done( |
| 204 | + running_workflow_id=running_workflow_id, |
| 205 | + success=success, |
| 206 | + ) |
0 commit comments