|
1 | | -import inspect |
2 | | -from logging import Logger |
3 | | -import simplejson as json |
4 | | - |
5 | 1 | from datetime import datetime |
| 2 | +from logging import Logger |
6 | 3 | from types import GeneratorType |
7 | 4 | from typing import Any, Dict, Generator, Iterable, List, TypeVar |
8 | 5 |
|
| 6 | +import simplejson as json |
| 7 | + |
9 | 8 | import durabletask.protos.helpers as ph |
10 | 9 | import durabletask.protos.orchestrator_service_pb2 as pb |
11 | | -from durabletask.task.activities import Activity, ActivityContext |
12 | 10 | import durabletask.task.task as task |
13 | | - |
| 11 | +from durabletask.task.activities import Activity, ActivityContext |
14 | 12 | from durabletask.task.orchestration import OrchestrationContext, Orchestrator |
15 | 13 | from durabletask.task.registry import Registry, get_name |
16 | 14 | from durabletask.task.task import Task |
@@ -67,11 +65,11 @@ def resume(self): |
67 | 65 | # case is if the user yielded on a WhenAll task and there are still |
68 | 66 | # outstanding child tasks that need to be completed. |
69 | 67 | if self._previous_task is not None: |
70 | | - if self._previous_task.is_failed(): |
| 68 | + if self._previous_task.is_failed: |
71 | 69 | # Raise the failure as an exception to the generator. The orchestrator can then either |
72 | 70 | # handle the exception or allow it to fail the orchestration. |
73 | 71 | self._generator.throw(self._previous_task.get_exception()) |
74 | | - elif self._previous_task.is_complete(): |
| 72 | + elif self._previous_task.is_complete: |
75 | 73 | # Resume the generator. This will either return a Task or raise StopIteration if it's done. |
76 | 74 | next_task = self._generator.send(self._previous_task.get_result()) |
77 | 75 | # TODO: Validate the return value |
@@ -138,6 +136,21 @@ def call_activity(self, activity: Activity[TInput, TOutput], *, |
138 | 136 | self._pending_tasks[id] = activity_task |
139 | 137 | return activity_task |
140 | 138 |
|
| 139 | + def call_sub_orchestrator(self, orchestrator: Orchestrator[TInput, TOutput], *, |
| 140 | + input: TInput | None = None, |
| 141 | + instance_id: str | None = None) -> task.Task[TOutput]: |
| 142 | + id = self.next_sequence_number() |
| 143 | + name = get_name(orchestrator) |
| 144 | + if instance_id is None: |
| 145 | + # Create a deteministic instance ID based on the parent instance ID |
| 146 | + instance_id = f"{self.instance_id}:{id:04x}" |
| 147 | + action = ph.new_create_sub_orchestration_action(id, name, instance_id, input) |
| 148 | + self._pending_actions[id] = action |
| 149 | + |
| 150 | + sub_orch_task = task.CompletableTask[TOutput]() |
| 151 | + self._pending_tasks[id] = sub_orch_task |
| 152 | + return sub_orch_task |
| 153 | + |
141 | 154 |
|
142 | 155 | class OrchestrationExecutor: |
143 | 156 | _generator: Orchestrator | None |
@@ -252,6 +265,45 @@ def process_event(self, ctx: RuntimeOrchestrationContext, event: pb.HistoryEvent |
252 | 265 | return |
253 | 266 | activity_task.fail(event.taskFailed.failureDetails) |
254 | 267 | ctx.resume() |
| 268 | + elif event.HasField("subOrchestrationInstanceCreated"): |
| 269 | + # This history event confirms that the sub-orchestration execution was successfully scheduled. |
| 270 | + # Remove the subOrchestrationInstanceCreated event from the pending action list so we don't schedule it again. |
| 271 | + task_id = event.eventId |
| 272 | + action = ctx._pending_actions.pop(task_id, None) |
| 273 | + if not action: |
| 274 | + raise _get_non_determinism_error(task_id, get_name(ctx.call_sub_orchestrator)) |
| 275 | + elif not action.HasField("createSubOrchestration"): |
| 276 | + expected_method_name = get_name(ctx.call_sub_orchestrator) |
| 277 | + raise _get_wrong_action_type_error(task_id, expected_method_name, action) |
| 278 | + elif action.createSubOrchestration.name != event.subOrchestrationInstanceCreated.name: |
| 279 | + raise _get_wrong_action_name_error( |
| 280 | + task_id, |
| 281 | + method_name=get_name(ctx.call_sub_orchestrator), |
| 282 | + expected_task_name=event.subOrchestrationInstanceCreated.name, |
| 283 | + actual_task_name=action.createSubOrchestration.name) |
| 284 | + elif event.HasField("subOrchestrationInstanceCompleted"): |
| 285 | + task_id = event.subOrchestrationInstanceCompleted.taskScheduledId |
| 286 | + sub_orch_task = ctx._pending_tasks.pop(task_id, None) |
| 287 | + if not sub_orch_task: |
| 288 | + # TODO: Should this be an error? When would it ever happen? |
| 289 | + self._logger.warning( |
| 290 | + f"Ignoring unexpected subOrchestrationInstanceCompleted event for '{ctx.instance_id}' with ID = {task_id}.") |
| 291 | + return |
| 292 | + result = None |
| 293 | + if not ph.is_empty(event.subOrchestrationInstanceCompleted.result): |
| 294 | + result = json.loads(event.subOrchestrationInstanceCompleted.result.value) |
| 295 | + sub_orch_task.complete(result) |
| 296 | + ctx.resume() |
| 297 | + elif event.HasField("subOrchestrationInstanceFailed"): |
| 298 | + task_id = event.subOrchestrationInstanceFailed.taskScheduledId |
| 299 | + sub_orch_task = ctx._pending_tasks.pop(task_id, None) |
| 300 | + if not sub_orch_task: |
| 301 | + # TODO: Should this be an error? When would it ever happen? |
| 302 | + self._logger.warning( |
| 303 | + f"Ignoring unexpected subOrchestrationInstanceFailed event for '{ctx.instance_id}' with ID = {task_id}.") |
| 304 | + return |
| 305 | + sub_orch_task.fail(event.subOrchestrationInstanceFailed.failureDetails) |
| 306 | + ctx.resume() |
255 | 307 | else: |
256 | 308 | eventType = event.WhichOneof("eventType") |
257 | 309 | raise OrchestrationStateError(f"Don't know how to handle event of type '{eventType}'") |
|
0 commit comments