2026-03-26 21:57:56 +00:00
|
|
|
"""Temporal workflow definitions for activity-core.
|
|
|
|
|
|
|
|
|
|
Two workflows are registered here:
|
|
|
|
|
- RunActivityWorkflow → orchestrator-tq
|
|
|
|
|
- TaskExecutorWorkflow → task-execution-tq
|
|
|
|
|
|
|
|
|
|
Workflow IDs follow the conventions in docs/conventions.md:
|
|
|
|
|
RunActivityWorkflow: activity-{activity_id}:{trigger_key}
|
|
|
|
|
TaskExecutorWorkflow: task-{run_id}:{task_type}:{index}
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
2026-03-26 22:25:19 +00:00
|
|
|
import uuid
|
2026-03-26 21:57:56 +00:00
|
|
|
from datetime import timedelta
|
|
|
|
|
|
|
|
|
|
from temporalio import workflow
|
2026-03-26 22:25:19 +00:00
|
|
|
from temporalio.common import RetryPolicy
|
2026-03-26 21:57:56 +00:00
|
|
|
|
|
|
|
|
with workflow.unsafe.imports_passed_through():
|
|
|
|
|
from activity_core.activities import (
|
|
|
|
|
load_activity_definition,
|
|
|
|
|
log_run,
|
|
|
|
|
resolve_context,
|
|
|
|
|
)
|
2026-03-26 22:25:19 +00:00
|
|
|
from activity_core.template_engine import evaluate_templates
|
|
|
|
|
|
|
|
|
|
_RETRY_POLICY = RetryPolicy(
|
|
|
|
|
initial_interval=timedelta(seconds=1),
|
|
|
|
|
backoff_coefficient=2.0,
|
|
|
|
|
maximum_interval=timedelta(minutes=5),
|
|
|
|
|
maximum_attempts=10,
|
|
|
|
|
)
|
2026-03-26 21:57:56 +00:00
|
|
|
|
2026-03-26 22:25:19 +00:00
|
|
|
_ACTIVITY_TIMEOUT = timedelta(minutes=5)
|
|
|
|
|
_TASK_QUEUE = "task-execution-tq"
|
2026-03-26 21:57:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@workflow.defn
|
|
|
|
|
class RunActivityWorkflow:
|
|
|
|
|
"""Durable orchestration workflow.
|
|
|
|
|
|
2026-03-26 22:25:19 +00:00
|
|
|
Sequence:
|
|
|
|
|
1. load_activity_definition(activity_id) → defn dict
|
|
|
|
|
2. resolve_context(defn.context_sources) → context snapshot
|
|
|
|
|
3. evaluate_templates(templates, context) → task specs (pure, no activity)
|
|
|
|
|
4. log_run(...) → run_id
|
|
|
|
|
5. start_child_workflow per task spec (fire-and-forget, detached)
|
2026-03-26 21:57:56 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@workflow.run
|
2026-03-26 22:25:19 +00:00
|
|
|
async def run(
|
|
|
|
|
self,
|
|
|
|
|
activity_id: str,
|
|
|
|
|
trigger_key: str,
|
|
|
|
|
scheduled_for: str | None = None,
|
|
|
|
|
) -> dict:
|
|
|
|
|
"""
|
|
|
|
|
Args:
|
|
|
|
|
activity_id: UUID of the ActivityDefinition row.
|
|
|
|
|
trigger_key: ISO-8601 datetime (cron) or event_id (event trigger).
|
|
|
|
|
Used as the idempotency key component.
|
|
|
|
|
scheduled_for: ISO-8601 string of the nominal scheduled time (cron only).
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
{"run_id": str, "tasks_spawned": int}
|
|
|
|
|
"""
|
|
|
|
|
# ── 1. Load definition ────────────────────────────────────────────────
|
|
|
|
|
defn: dict = await workflow.execute_activity(
|
|
|
|
|
load_activity_definition,
|
|
|
|
|
activity_id,
|
|
|
|
|
start_to_close_timeout=_ACTIVITY_TIMEOUT,
|
|
|
|
|
retry_policy=_RETRY_POLICY,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ── 2. Resolve context ────────────────────────────────────────────────
|
|
|
|
|
context_snapshot: dict = await workflow.execute_activity(
|
|
|
|
|
resolve_context,
|
|
|
|
|
defn["context_sources"],
|
|
|
|
|
start_to_close_timeout=_ACTIVITY_TIMEOUT,
|
|
|
|
|
retry_policy=_RETRY_POLICY,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ── 3. Evaluate templates (pure — no activity) ────────────────────────
|
|
|
|
|
task_specs: list[dict] = evaluate_templates(
|
|
|
|
|
defn["task_templates"], context_snapshot
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ── 4. Log the run ────────────────────────────────────────────────────
|
|
|
|
|
# run_id is derived deterministically so log_run retries are idempotent.
|
|
|
|
|
run_id = str(
|
|
|
|
|
uuid.uuid5(uuid.NAMESPACE_URL, f"{activity_id}:{trigger_key}")
|
|
|
|
|
)
|
|
|
|
|
await workflow.execute_activity(
|
|
|
|
|
log_run,
|
|
|
|
|
{
|
|
|
|
|
"run_id": run_id,
|
|
|
|
|
"activity_id": activity_id,
|
|
|
|
|
"scheduled_for": scheduled_for,
|
|
|
|
|
"context_snapshot": context_snapshot,
|
|
|
|
|
"tasks_spawned": len(task_specs),
|
|
|
|
|
"version_used": defn["version"],
|
|
|
|
|
},
|
|
|
|
|
start_to_close_timeout=_ACTIVITY_TIMEOUT,
|
|
|
|
|
retry_policy=_RETRY_POLICY,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# ── 5. Spawn task executor children (fire-and-forget) ─────────────────
|
|
|
|
|
for index, spec in enumerate(task_specs):
|
|
|
|
|
child_id = f"task-{run_id}:{spec['task_type']}:{index}"
|
|
|
|
|
await workflow.start_child_workflow(
|
|
|
|
|
TaskExecutorWorkflow,
|
|
|
|
|
args=[run_id, spec["task_type"], spec["params"]],
|
|
|
|
|
id=child_id,
|
|
|
|
|
task_queue=_TASK_QUEUE,
|
|
|
|
|
parent_close_policy=workflow.ParentClosePolicy.ABANDON,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return {"run_id": run_id, "tasks_spawned": len(task_specs)}
|
2026-03-26 21:57:56 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@workflow.defn
|
|
|
|
|
class TaskExecutorWorkflow:
|
|
|
|
|
"""Child workflow that executes one concrete task instance.
|
|
|
|
|
|
2026-03-26 22:25:19 +00:00
|
|
|
Stub implementation — T19.
|
2026-03-26 21:57:56 +00:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
@workflow.run
|
|
|
|
|
async def run(self, run_id: str, task_type: str, params: dict) -> dict:
|
|
|
|
|
raise NotImplementedError("T19")
|