feat(workflows): implement RunActivityWorkflow — T18

workflows.py — RunActivityWorkflow:
  1. load_activity_definition(activity_id)
  2. resolve_context(context_sources)
  3. evaluate_templates (pure, called in-workflow)
  4. log_run({run_id, ...}) — run_id = uuid5(NAMESPACE_URL, activity_id:trigger_key)
  5. start_child_workflow(TaskExecutorWorkflow, ...) per task spec
     ABANDON parent-close policy (fire-and-forget)
  Returns {"run_id": str, "tasks_spawned": int}

activities.py — log_run updated:
  - now accepts run_id in run_payload (deterministic, passed from workflow)
  - uses pg INSERT ... ON CONFLICT (run_id) DO NOTHING for idempotency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-26 22:25:19 +00:00
parent 068780224e
commit da7de6ea3b
3 changed files with 109 additions and 23 deletions

View File

@@ -15,6 +15,7 @@ import uuid
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.dialects.postgresql import insert as pg_insert
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from temporalio import activity
from temporalio.exceptions import ApplicationError
@@ -120,7 +121,11 @@ async def resolve_context(context_sources: list[dict]) -> dict:
async def log_run(run_payload: dict) -> str:
"""Persist an ActivityRun record to Postgres and return its run_id.
Idempotent: uses INSERT … ON CONFLICT (run_id) DO NOTHING so Temporal
activity retries do not produce duplicate rows.
Expected keys in run_payload:
run_id (str UUID — computed deterministically in workflow)
activity_id (str UUID)
scheduled_for (ISO-8601 str or None)
context_snapshot (dict)
@@ -132,21 +137,28 @@ async def log_run(run_payload: dict) -> str:
"""
Session = _get_session_factory()
run_id = uuid.UUID(run_payload["run_id"])
scheduled_for: datetime | None = None
if run_payload.get("scheduled_for"):
scheduled_for = datetime.fromisoformat(run_payload["scheduled_for"])
row = ActivityRun(
activity_id=uuid.UUID(run_payload["activity_id"]),
scheduled_for=scheduled_for,
fired_at=datetime.now(tz=timezone.utc),
context_snapshot=run_payload["context_snapshot"],
tasks_spawned=run_payload["tasks_spawned"],
version_used=run_payload["version_used"],
stmt = (
pg_insert(ActivityRun)
.values(
run_id=run_id,
activity_id=uuid.UUID(run_payload["activity_id"]),
scheduled_for=scheduled_for,
fired_at=datetime.now(tz=timezone.utc),
context_snapshot=run_payload["context_snapshot"],
tasks_spawned=run_payload["tasks_spawned"],
version_used=run_payload["version_used"],
)
.on_conflict_do_nothing(index_elements=["run_id"])
)
async with Session() as session:
async with session.begin():
session.add(row)
await session.execute(stmt)
return str(row.run_id)
return str(run_id)