Job transport¶
The REST API has two ways to run a job, and one contract over them:
ada.comms.rest.job_transport.JobTransport. It is built once, at app build
time, from whether a queue URL is configured, and carried on
RestContext.jobs — so no request ever asks which shape the deployment is.
QueueJobTransportA NATS-backed worker pool. Every job kind is available. Jobs are enqueued onto the pool advertising the needed capability and their progress lives in the queue’s KV bucket.
LocalJobTransportNo queue. Plugin jobs run in this process, in a thread (
ada.comms.rest.local_jobs); every other job kind is unavailable.
Why the asymmetry is the contract¶
The right shape for a deployment is the wrong shape for one person running the
viewer on a laptop. A plugin’s backend job is long and CPU-heavy and wants its
own pod — but with no NATS, POST /api/plugins/{id}/jobs used to answer 503
and the plugin’s “run” button was dead in exactly the setup the examples put
you in. So a plugin job runs locally instead, through the same
submit(JobRequest) the queue path uses: same entrypoint, same synchronous
storage facade, same on_progress and cancel_event, and a job id that
GET /api/convert/{job_id} serves. The plugin cannot tell the difference.
Nothing else can run locally, and that is a statement rather than an omission:
a conversion needs a worker’s CAD stack, a procedural build needs its engine’s
pool. LocalJobTransport reports those as unavailable so the refusal is one
fact in one place instead of a branch each route remembers or forgets.
The contract¶
Gating — one call, and one 503 text per feature:
supports(feature) -> boolWhether this transport can run that kind of work.
unavailable(feature)Raise the 503 for a feature it cannot. The detail strings live in
FEATURE_UNAVAILABLE_DETAILand are API surface: clients and tests read them, so they are pinned rather than generated from the feature name.require(feature)supportsorunavailable.
Jobs:
submit(req, *, before_dispatch=None) -> SubmittedJobRun or enqueue a
JobRequest; raises the feature’s 503 if unsupported, so a route that forgets the gate still refuses correctly.before_dispatchis awaited between the job becoming durable and becoming visible to anything that would run it — the window in which a caller writes its audit row, because the worker’s own audit writes are bareUPDATE ... WHERE job_idand a fast job can outrun the API’s INSERT. A transport with no dispatch step does not call it.inprocess(job_id)The in-process job with this id, or
None. Synchronous and free; the queue transport always answersNonewithout touching the network.status(job_id)/cancel(job_id)The job’s current state, and a request to stop it. In-process cancellation is real (the entrypoint holds the same
cancel_event); queued cancellation nudges the KV record and the worker runs to completion.
Capability reporting:
capabilities(), advertised_specs(field, fallback_field=None),
local_specs(), worker_image_tag()
What a route may truthfully say is available. Without a pool the first two are empty and
local_specs()returns what this process registered — online by definition, being the thing that would run the job.
Queue-only features¶
Everything except plugin_jobs:
bake, bbox_inference, component_build, conversion,
job_status_report, procedural_build, procedural_export,
procedural_import, procedural_relocations, result_meta,
utilities, worker_registry.
Not a transport concern¶
Things that need a queue as storage rather than as a way to run work stay on
RestContext.queue: the KV meta keyspace, the compression-sweep state, the
completed-job purges, and the worker-registry refresh loop that fills the
snapshot the transport reads. Folding those in would make this the place where
“is NATS configured” is asked, rather than the place where “can this job run”
is answered.