app.orchestrator¶
Job → Task DAG construction + cache-key computation.
A Job is the user-facing intent (one HTTP call). It owns a DAG of Task`s. Each Task has a `cache_key derived from (kind, inputs_hash, params_hash, runtime_version_id); if a Task with that cache_key has already produced output, the DAG short-circuits to the existing result without enqueuing.
This module builds DAGs deterministically from input specs. It does not talk to the DB or the queue — those are scheduler concerns.
- class app.orchestrator.dag.TaskNode(task_id: 'str', kind: 'str', inputs_hash: 'str', params_hash: 'str', depends_on: 'list[str]' = <factory>, gpu_required: 'bool' = True, metadata: 'dict' = <factory>)[source]
Bases:
object- Parameters:
task_id (str)
kind (str)
inputs_hash (str)
params_hash (str)
depends_on (list[str])
gpu_required (bool)
metadata (dict)
- task_id: str
- kind: str
- inputs_hash: str
- params_hash: str
- depends_on: list[str]
- gpu_required: bool = True
- metadata: dict
- cache_key(runtime_version_id)[source]
- Parameters:
runtime_version_id (str)
- Return type:
str
- class app.orchestrator.dag.JobDag(nodes: 'list[TaskNode]')[source]
Bases:
object- Parameters:
nodes (list[TaskNode])
- nodes: list[TaskNode]
- topo_order()[source]
- Return type:
list[TaskNode]
- app.orchestrator.dag.hash_params(spec)[source]
- Parameters:
spec (dict)
- Return type:
str
- app.orchestrator.dag.hash_inputs(refs)[source]
- Parameters:
refs (dict)
- Return type:
str
Scheduler — submit job DAG and enqueue tasks for execution.
Persists Job + Task rows and hands each non-cached task off to the
configured queue (see app.orchestrator.queue.get_queue). Whether
the queue is ARQ-backed or in-process is a runtime decision driven by
settings.queue_backend / settings.inline_tasks.
- async app.orchestrator.scheduler.submit_job_dag(session, *, tenant_id, project_id, recipe, spec, nodes, inline=False)[source]
Persist Job + Task rows and return them.
inline=Trueforces the InlineQueue regardless of settings (used by tests). ARQ enqueue failures (e.g. Redis absent in dev) are suppressed — the tasks remainpendingand will be picked up on the next worker boot.- Parameters:
session (AsyncSession)
tenant_id (str)
project_id (str)
recipe (str)
spec (dict | None)
nodes (list[TaskNode])
inline (bool)
- Return type:
tuple[str, list[Task]]
Lease helpers — works on SQLite and Postgres.
Postgres has SELECT … FOR UPDATE SKIP LOCKED; SQLite does not. We use the portable subset: an UPDATE that increments lease only when the existing lease has expired, then check rowcount. This works on both engines without dialect branches.
- app.orchestrator.lease.now_utc()[source]
- Return type:
datetime
- async app.orchestrator.lease.try_acquire_lease(session, *, table, pk_col, lease_col, worker_col, pk_value, worker_id, ttl_seconds)[source]
- Parameters:
session (AsyncSession)
pk_value (str)
worker_id (str)
ttl_seconds (int)
- Return type:
bool
- async app.orchestrator.lease.refresh_lease(session, *, table, pk_col, lease_col, worker_col, pk_value, worker_id, ttl_seconds)[source]
- Parameters:
session (AsyncSession)
pk_value (str)
worker_id (str)
ttl_seconds (int)
- Return type:
bool
Fair-share scheduler.
Picks the next ready Task across all tenants, biased toward tenants with fewer recent admissions. The interleaving rule:
Pick the tenant with the smallest count of currently-running tasks; break ties by the tenant whose last-admitted task is oldest. Within a tenant, pick by Task.created_at (FIFO).
- A simpler invariant we expose for testing:
max_consecutive_jobs_per_tenant: never admit more than N consecutive Tasks from the same tenant when other tenants have ready work.
This is intentionally a picker — it does not enqueue. The supervisor calls pick_next_task(…) between subprocess slots; in tests we use the picker directly to validate fairness.
- class app.orchestrator.fair_share.FairShareState(max_consecutive_per_tenant: 'int' = 2, recent_tenants: 'deque[str]' = <factory>)[source]
Bases:
object- Parameters:
max_consecutive_per_tenant (int)
recent_tenants (deque[str])
- max_consecutive_per_tenant: int = 2
- recent_tenants: deque[str]
- admit(tenant_id)[source]
- Parameters:
tenant_id (str)
- Return type:
None
- consecutive_for(tenant_id)[source]
- Parameters:
tenant_id (str)
- Return type:
int
- async app.orchestrator.fair_share.pick_next_task(session, *, state)[source]
Find the highest-priority ready Task that doesn’t violate max_consecutive_per_tenant. Ready = pending + deps satisfied.
For Phase 5 v1 we treat ‘ready’ as pending with no remaining deps; dep satisfaction is a precondition the orchestrator already enforces when emitting tasks. Tenants are weighted by current running count.
- Parameters:
session (AsyncSession)
state (FairShareState)
- Return type:
Task | None
Resume a previously failed/cancelled Job.
- The resume contract:
Tasks in (failed, cancelled, cancelled_dirty) are reset to pending. Their cache_key is preserved so the same upstream cache hits replay.
Tasks in succeeded are kept as-is (free cache).
The Job itself transitions back to pending, cancel flags cleared.
If inline=True (or SFMAPI_INLINE_TASKS=true), reset tasks are re-run synchronously here. Otherwise they’re re-enqueued via ARQ.
For mapping tasks specifically, the worker reads jobs/{job_id}/checkpoints/ (see app.storage.mapping_input) to resume from the latest MappingInput.save write, avoiding a full restart.
- async app.orchestrator.resume.resume_job(session, *, tenant_id, job_id, inline=None)[source]
- Parameters:
session (AsyncSession)
tenant_id (str)
job_id (str)
inline (bool | None)
- Return type:
Job