| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171 |
- """Single-use task-token ledger interfaces and an in-memory test implementation."""
- from __future__ import annotations
- import threading
- from dataclasses import dataclass
- from typing import Mapping, Optional
- from sqlalchemy import text
- @dataclass
- class TaskLedgerRecord:
- jti: str
- binding: Mapping[str, object]
- expires_at: int
- status: str = "running"
- commit_outcome: str = "not_applicable"
- safe_detail: str = ""
- class InMemoryTaskLedger:
- def __init__(self):
- self._records = {}
- self._lock = threading.Lock()
- def claim(self, jti, binding, *, expires_at):
- with self._lock:
- if jti in self._records:
- return False
- self._records[jti] = TaskLedgerRecord(
- jti=str(jti),
- binding=dict(binding),
- expires_at=int(expires_at),
- )
- return True
- def finish(
- self,
- jti,
- *,
- status,
- commit_outcome="not_applicable",
- safe_detail="",
- ):
- with self._lock:
- record = self._records[str(jti)]
- record.status = str(status)
- record.commit_outcome = str(commit_outcome)
- record.safe_detail = str(safe_detail)[:500]
- def get(self, jti) -> Optional[TaskLedgerRecord]:
- with self._lock:
- return self._records.get(str(jti))
- class PostgresTaskLedger:
- """Durable, cross-worker single-use ledger backed by the platform DB."""
- def __init__(self, engine):
- self.engine = engine
- def claim(self, jti, binding, *, expires_at):
- parameters = {
- "jti": str(jti),
- "task_uid": binding["task_uid"],
- "dataflow_uid": binding["dataflow_uid"],
- "workflow_version": int(binding["workflow_version"]),
- "correlation_id": binding["correlation_id"],
- "node_id": binding["node_id"],
- "node_type": binding["node_type"],
- "data_source_uid": binding.get("data_source_uid"),
- "idempotency_key": binding.get("idempotency_key"),
- "expires_at": int(expires_at),
- }
- with self.engine.begin() as connection:
- result = connection.execute(
- text(
- """
- INSERT INTO public.runner_task_executions (
- token_jti, task_uid, dataflow_uid, workflow_version,
- correlation_id, node_id, node_type, data_source_uid,
- idempotency_key, status, commit_outcome, expires_at
- ) VALUES (
- CAST(:jti AS uuid), CAST(:task_uid AS uuid),
- CAST(:dataflow_uid AS uuid), :workflow_version,
- CAST(:correlation_id AS uuid), :node_id, :node_type,
- CAST(:data_source_uid AS uuid), :idempotency_key,
- 'running', 'not_applicable',
- to_timestamp(:expires_at)
- )
- ON CONFLICT DO NOTHING
- """
- ),
- parameters,
- )
- return int(result.rowcount or 0) == 1
- def finish(
- self,
- jti,
- *,
- status,
- commit_outcome="not_applicable",
- safe_detail="",
- ):
- allowed_statuses = {"success", "failed", "unknown"}
- allowed_outcomes = {
- "not_applicable",
- "not_committed",
- "committed",
- "unknown",
- }
- if status not in allowed_statuses or commit_outcome not in allowed_outcomes:
- raise ValueError("runner task outcome is invalid")
- with self.engine.begin() as connection:
- connection.execute(
- text(
- """
- UPDATE public.runner_task_executions
- SET status = :status,
- commit_outcome = :commit_outcome,
- safe_detail = :safe_detail,
- finished_at = CURRENT_TIMESTAMP
- WHERE token_jti = CAST(:jti AS uuid)
- AND status = 'running'
- """
- ),
- {
- "jti": str(jti),
- "status": status,
- "commit_outcome": commit_outcome,
- "safe_detail": str(safe_detail)[:500],
- },
- )
- def get(self, jti):
- with self.engine.begin() as connection:
- row = (
- connection.execute(
- text(
- """
- SELECT token_jti::text AS jti, task_uid::text,
- node_id, data_source_uid::text,
- idempotency_key, status, commit_outcome,
- safe_detail, EXTRACT(EPOCH FROM expires_at)::bigint
- AS expires_at
- FROM public.runner_task_executions
- WHERE token_jti = CAST(:jti AS uuid)
- """
- ),
- {"jti": str(jti)},
- )
- .mappings()
- .one_or_none()
- )
- if row is None:
- return None
- return TaskLedgerRecord(
- jti=row["jti"],
- binding={
- "task_uid": row["task_uid"],
- "node_id": row["node_id"],
- "data_source_uid": row["data_source_uid"],
- "idempotency_key": row["idempotency_key"],
- },
- expires_at=int(row["expires_at"]),
- status=row["status"],
- commit_outcome=row["commit_outcome"],
- safe_detail=row["safe_detail"] or "",
- )
|