Compute¶
Bounded context for scheduling and tracking compute tasks on external schedulers
(Slurm, local runner, Kubernetes, …). The domain vocabulary is scheduler-neutral:
TaskRunSucceeded / TaskRunFailed never mention sbatch or squeue.
Concrete schedulers live exclusively in the driven adapters.
Overview¶
Two aggregates¶
| Entity | Purpose |
|---|---|
TaskSpec |
Immutable specification of what to run — command, resources, retry policy, labels. Carries no runtime state. |
TaskRun |
A concrete execution of a TaskSpec. Evolves via apply_task_event() through a well-defined lifecycle. |
Lifecycle¶
CREATED
│ TaskDispatchRequested
▼
DISPATCH_REQUESTED
│ TaskSubmitted
▼
SUBMITTED ──► QUEUED ──► RUNNING ──► SUCCEEDED
└──► FAILED / CANCELLED / TIMED_OUT
/ OUT_OF_MEMORY / INFRA_FAILED / PREEMPTED
UNKNOWN is used when the scheduler reports a state that cannot be mapped to any
of the above. TaskRun.is_terminal returns True for all terminal statuses.
Event flow¶
TaskDispatchRequested
│
▼
TaskDispatchRequestedHandler
│ calls
▼
JobDispatcher.submit(DispatchRequest)
│
▼
SlurmJobDispatcher → sbatch → ExternalJobRef
│
▼
TaskSubmitted(external_ref=ExternalJobRef(...))
Later — SlurmJobDispatcher.poll() → squeue / sacct
│
▼
JobObservation(state=RUNNING | SUCCEEDED | FAILED | ...)
│
▼
JobObservationHandler → domain_event_from_observation()
│
▼
TaskRunStarted / TaskRunSucceeded / TaskRunFailed
Scheduler vocabulary mapping¶
Adapters translate native scheduler states before they cross into the domain:
| Slurm state | ExternalJobState |
Domain event |
|---|---|---|
PD / PENDING |
QUEUED |
TaskQueued |
R / RUNNING, CG |
RUNNING |
TaskRunStarted |
CD / COMPLETED |
SUCCEEDED |
TaskRunSucceeded |
F / FAILED |
FAILED |
TaskRunFailed(FAILED) |
CA / CANCELLED |
CANCELLED |
TaskRunFailed(CANCELLED) |
TO / TIMEOUT |
TIMED_OUT |
TaskRunFailed(TIMED_OUT) |
OOM / OUT_OF_MEMORY |
OUT_OF_MEMORY |
TaskRunFailed(OUT_OF_MEMORY) |
NF / BF / NODE_FAIL |
INFRA_FAILED |
TaskRunFailed(INFRA_FAILED) |
PR / PREEMPTED |
PREEMPTED |
TaskRunFailed(PREEMPTED) |
Design rules¶
apply_task_event()is idempotent: re-applying an already-applied event returns the run unchanged.- Events target a different
run_idare silently ignored. - Events arriving after the run is terminal are silently ignored.
JobObservationis an integration DTO, not a domain event. Onlydomain_event_from_observation()promotes it to an event.ExternalJobRefis intentionally generic — it never says "Slurm".
Usage example¶
from pathlib import Path
from iris.domain.compute.entities import TaskRun, TaskSpec
from iris.domain.compute.events import TaskDispatchRequested
from iris.domain.compute.handlers import JobObservationHandler, TaskDispatchRequestedHandler
from iris.domain.compute.objects import CommandSpec, DispatchHints, ResourceRequest
from iris.adapters.driven.dispatchers.slurm import SlurmJobDispatcher
from iris.adapters.driven.repositories.redis_compute import make_redis_task_run_repository
spec = TaskSpec(
name="vcf_norm_chr22",
command=CommandSpec(
argv=["bcftools", "norm", "-f", "/ref/GRCh38.fa", "-Oz", "-o", "out.vcf.gz", "in.vcf.gz"],
workdir=Path("/scratch/project/run_001"),
env={"OMP_NUM_THREADS": "4"},
),
resources=ResourceRequest(ranks=1, cpus_per_rank=4, memory_mb=16_000, time_limit_s=7200),
hints=DispatchHints(queue="short", project="bioinfo"),
labels={"workflow": "prs-preprocess", "chrom": "22"},
)
repo = make_redis_task_run_repository()
dispatcher = SlurmJobDispatcher(submit_dir=Path("/scratch/project/.dispatch"))
run = TaskRun(spec=spec)
repo.save(run)
# Dispatch
event = TaskDispatchRequested(task_id=spec.task_id, run_id=run.run_id)
new_events = TaskDispatchRequestedHandler(repo=repo, dispatcher=dispatcher).handle(event)
# Poll (from a poller / cron)
run = repo.get(run.run_id)
for obs in dispatcher.poll([run.external_ref]):
JobObservationHandler(repo=repo).handle(obs)
Entities¶
iris.domain.compute.entities
¶
Compute domain entities: TaskSpec (intent) and TaskRun (execution).
TaskSpec
¶
Immutable specification of what to run.
A TaskSpec carries no runtime state; it defines intent. Each concrete
execution is a :class:TaskRun.
TaskRun
¶
A concrete execution of a :class:TaskSpec.
Immutable like every domain object: state changes are produced with
attrs.evolve() inside :func:~iris.domain.compute.services.apply_task_event,
which keeps event handling idempotent and reproducible.
Objects¶
iris.domain.compute.objects
¶
Compute domain value objects: enums, resource requests, command specs, and helpers.
This module models scheduler-neutral concepts for running compute tasks. The
domain vocabulary (TaskStatus, ExternalJobState) is deliberately generic:
concrete schedulers such as Slurm live in the driven adapters and translate their
native states into these domain terms.
TaskStatus
¶
Bases: str, Enum
Lifecycle status of a :class:~iris.domain.compute.entities.TaskRun.
ExternalJobState
¶
Bases: str, Enum
Scheduler-neutral state of an external job as reported by a dispatcher.
Adapters translate their native scheduler states (e.g. Slurm PD, R,
CD) into these values inside a :class:JobObservation.
CommandSpec
¶
An immutable command to execute: argv, optional workdir, and environment.
ResourceRequest
¶
Scheduler-neutral compute resource request for a task.
ranks is the domain-level process/rank count. In Slurm this usually maps
to --ntasks.
DispatchHints
¶
Scheduler-neutral scheduling hints.
A Slurm adapter may map queue → --partition, project →
--account, and quality → --qos. extra is an escape hatch for
rare, adapter-scoped options (e.g. ("slurm.exclusive", "true")) and
should be used sparingly.
RetryPolicy
¶
Retry policy governing when a failed task run should be retried.
ExternalJobRef
¶
Opaque reference to a job in an external scheduler.
Generic by design: the domain never names it SlurmJobRef. The
dispatcher field identifies which adapter owns the reference.
ExitStatus
¶
Exit information for a finished job: exit code, signal, and/or message.
JobObservation
¶
Integration DTO describing an external job's observed state at a point in time.
Not a domain event. Adapters (e.g. SlurmJobDispatcher) build this from
squeue / sacct output; the observation handler translates it into
domain events.
utcnow
¶
new_id
¶
as_str_tuple
¶
Convert an iterable to a tuple of strings, or empty tuple when None.
as_kv_tuple
¶
Convert a mapping or pair-iterable to a sorted tuple of (str, str) pairs.
Source code in src/iris/domain/compute/objects.py
positive_int
¶
Validate that value is an integer greater than or equal to 1.
Source code in src/iris/domain/compute/objects.py
non_negative_int
¶
Validate that value is an integer greater than or equal to 0.
Source code in src/iris/domain/compute/objects.py
optional_positive_int
¶
Validate that value is None or an integer greater than or equal to 1.
Source code in src/iris/domain/compute/objects.py
non_empty_argv
¶
Validate that a command argv tuple is not empty.
Source code in src/iris/domain/compute/objects.py
Events¶
iris.domain.compute.events
¶
Domain events for the compute bounded context.
Events are informative: they carry only the relevant identifiers plus event
metadata (occurred_at, correlation and causation IDs). They never embed a
snapshot of the aggregate. The domain speaks in terms of TaskRunSucceeded /
TaskRunFailed; scheduler-specific states (Slurm PD, R, CD, …)
never appear here — adapters translate them before events are emitted.
Services¶
iris.domain.compute.services
¶
Compute domain services: aggregate transitions and observation translation.
These are pure functions over immutable aggregates. apply_task_event folds a
domain event onto a :class:TaskRun (event-sourcing style), and is idempotent:
re-applying an already-applied event returns the run unchanged.
domain_event_from_observation maps a scheduler-neutral
:class:JobObservation into the appropriate domain event.
apply_task_event
¶
Return a new :class:TaskRun with event applied.
Idempotent and guarded: events targeting a different run_id or arriving
after the run is terminal are ignored (the run is returned unchanged).
| PARAMETER | DESCRIPTION |
|---|---|
run
|
The current run aggregate.
TYPE:
|
event
|
A compute domain event to fold onto the run.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
TaskRun
|
The evolved run, or the same instance if the event is a no-op. |
| RAISES | DESCRIPTION |
|---|---|
ValueError
|
If a :class: |
Source code in src/iris/domain/compute/services.py
domain_event_from_observation
¶
Translate a :class:JobObservation into a domain event, or None.
Returns None when the observation does not apply (mismatched reference, terminal run, or a state that implies no transition from the run's current status).
| PARAMETER | DESCRIPTION |
|---|---|
run
|
The run the observation is believed to belong to.
TYPE:
|
obs
|
The scheduler-neutral observation.
TYPE:
|
| RETURNS | DESCRIPTION |
|---|---|
DomainEvent | None
|
The corresponding domain event, or None if no transition applies. |
Source code in src/iris/domain/compute/services.py
Handlers¶
iris.domain.compute.handlers
¶
Compute domain event handlers.
Handlers orchestrate the interaction between the aggregate, the repository, and
the :class:JobDispatcher port. They translate between the domain vocabulary and
the dispatcher boundary without leaking scheduler-specific concepts into the
domain.
TaskDispatchRequestedHandler
¶
Handle :class:TaskDispatchRequested by submitting to the dispatcher.
handle
¶
Submit the run to the dispatcher and emit :class:TaskSubmitted.
Idempotent: if the run already carries an external_ref it is not
submitted again. Returns the newly produced events (empty if none).
Source code in src/iris/domain/compute/handlers.py
JobObservationHandler
¶
Translate a :class:JobObservation into domain events and persist them.
handle
¶
Route obs to its run, apply the derived event, and return it.
Returns an empty list when the observation does not match any run or implies no transition.
Source code in src/iris/domain/compute/handlers.py
Repositories¶
iris.domain.compute.repositories
¶
Repository interface for the compute domain.
TaskRunRepository
¶
Bases: Repository[TaskRun]
Repository for :class:TaskRun aggregates.
Extends the generic read contract (get) with persistence and a lookup by
external scheduler reference, used by the observation handler to route
incoming :class:~iris.domain.compute.objects.JobObservation DTOs back to
their originating run.
Ports¶
Job dispatcher¶
iris.domain.compute.ports.driven.job_dispatcher
¶
Driven port for dispatching compute jobs to an external scheduler.
This port is the boundary with concrete schedulers (Slurm, a local runner,
Kubernetes, …). It knows nothing about any specific backend: adapters translate
native scheduler states into the domain's :class:ExternalJobState /
:class:JobObservation vocabulary.
DispatchRequest
¶
Flattened, scheduler-neutral request handed to a :class:JobDispatcher.
from_run
classmethod
¶
Build a DispatchRequest from a :class:TaskRun.
Source code in src/iris/domain/compute/ports/driven/job_dispatcher.py
JobDispatcher
¶
Bases: ABC
Abstract port for submitting, cancelling, and polling external jobs.
Concrete implementations (e.g. SlurmJobDispatcher) live in the driven
adapters. The name attribute identifies the dispatcher and is stamped
onto every :class:ExternalJobRef it produces.