On this page

Reservation

A host runs more than queue jobs. Stado places a Jeden session on it with stado workload attach and a browser task with stado workload run. Without a hold, the host would keep publishing itself as free and could be handed any number of sessions while the scheduler read it as idle. A reservation is the hold a placed workload keeps on its host while it runs, so the host publishes what is really left.

A detached session holds its host a different way. stado workload start submits the session as a queue job carrying the kind's cpu_cores and ram_gb as the job's own declared sizing, so the worker admits it only while that much is really free and subtracts it for as long as the session runs. There is no reservation document for it: the running job is the hold, and it ends when the job does, with no caller left to heartbeat anything.

What it is

One JSON document per placed workload, under state/reservations/<consumer_id>/<reservation_id>.json:

{
  "schema_version": 1,
  "reservation_id": "e241070e-407b-44b8-98a4-622b2b3c47a0",
  "consumer_id": "local-example-host.local",
  "target": "example-host",
  "kind": "weles-diagnostics",
  "product": "weles-worker",
  "holder": "operator@workstation hold 2s pid 31618",
  "cpu_cores": 1,
  "ram_gb": 1.0,
  "vram_gb": 0,
  "acquired_at": "2026-09-17T06:10:02Z",
  "heartbeat_at": "2026-09-17T06:10:02Z",
  "ttl_seconds": 180
}

The sizes come from the workload declaration, never from the caller: stado-rs/data/work/workloads.json carries a reservation per kind — a Jeden session holds 2 cores and 4 GiB, a browser task 2 cores and 3 GiB, a diagnostic read 1 core and 1 GiB. A kind that declares none is refused with workload kind '<KIND>' declares no reservation; add it to stado-rs/data/work/workloads.json.

A reservation is live while heartbeat_at + ttl_seconds is in the future. The holder heartbeats it every 60 seconds; the TTL is three heartbeats, so one missed heartbeat is a slow store and three is a holder that is gone.

Who declares it

The path that places the work, immediately before it starts the process: stado workload attach takes the hold after the host's readiness probe passes and before jeden rpc starts; stado workload run takes it after placement and before the runner; stado capacity hold takes it on purpose, for a fixed time. All three go through one function, so a kind cannot be placed without its hold. The hold is released when the process ends; a process that dies without releasing leaves a row that expires by itself.

The holder starts with <account>@<host>, then names the path and its pid. The account is read as a job's submitter is (see Jobs): USER, else LOGNAME, else the process's account in the system's user database. When none can be read the placing path refuses with that reason instead of taking a hold in a made-up name; a refusal that cannot name its requester is not recorded under state/fleet/unmet/, and stderr says why.

Before taking the hold, the placing path reads the host's latest capacity publication. A host whose net room is below the kind's declaration is refused with the sentence

<target> has no room for <kind>: needs <c> cores and <r> GiB, host publishes <c2> cores and <r2> GiB net of <n> reservation(s); pick another host with --target or wait for a reservation to end

and a host that publishes accepting_jobs: false is refused with <target> is not accepting placements (<admission_reason>); pick another host with --target or wait for it to clear. A fresh publication that does not state available_cpu_cores or accepting_jobs is refused with <target> publishes no <field> in its capacity row, so whether the hold fits cannot be judged; update its agent or pick another host with --target, because reading either as none would refuse for a reason the host never gave. A free RAM or reservation count the publication leaves out is printed as not reported in the no-room sentence. Every refusal is recorded under state/fleet/unmet/ so stado fleet needs can count them. An unpinned Jeden session moves on to the next candidate host after a refusal, exactly as it does after a failed readiness probe.

A host with no fresh publication is taken on trust: its agent is silent, and refusing every placement on a quiet host would make the primitive a liability the day a store hiccups. The hold is still written, so the next publication subtracts it.

Who observes it

The host's own agent, on every poll. Both writers of a capacity document — the agent tick and the heartbeat republisher — pass through one publish function, which reads the host's live reservations and subtracts them: available_cpu_cores, free_ram_gb and free_vram_gb are published net of every live hold, the measured figures stay in diag.measured_available_cpu_cores, diag.measured_free_ram_gb and diag.measured_free_vram_gb, and the document gains running_workloads, reserved and reservations:

"running_workloads": 1,
"reserved": {"cpu_cores": 2, "ram_gb": 4.0, "vram_gb": 0},
"reservations": [{"reservation_id": "…", "kind": "jeden-session", "product": "jeden",
                  "holder": "operator@workstation jeden-session wisent-compute pid 4711",
                  "cpu_cores": 2, "ram_gb": 4.0, "vram_gb": 0, "acquired_at": "…"}]

When the net leaves less than one core or one GiB, the host publishes accepting_jobs: false with diag.admission_reason: reservations_exhausted. The scheduler, stado host gates and every other reader of the publication see the same number; nothing reads the reservations a second way.

The agent also retires every expired row older than an hour, in the same listing it reads the live ones from. A row that just expired stays for the hour so stado capacity reservations can still show what ended.

Where it lives

state/reservations/<consumer_id>/, in the canonical object store. It is under state/ because that is the root the object gateway authorizes for fleet state; a prefix no namespace policy declares is refused with 401 unauthorized or non-immutable release write, on every write, while reads keep working.

Commands

stado capacity list [--json]                    # every host, net of its reservations
stado capacity reservations [TARGET] [--json]   # the holds, live or just ended
stado capacity hold --kind KIND --target TARGET --seconds N [--json]

list prints one row per publication: target, whether it accepts work, running jobs, running workloads, CPU and RAM and VRAM free/total, what is reserved, the admission reason and the publication's age. A field the publication does not carry (one written before workloads or reservations were published) prints as not reported, and is null with --json, instead of a zero or an empty list. reservations prints the holds; with no target it prints every host's, and <target> holds no reservations or no host holds a reservation when there are none. hold takes a kind's declared hold on a host, heartbeats it for N seconds and releases it, printing the receipt with --json; it is refused with the sentences above when the host has no room.

stado host gates TARGET prints a reserved: line and one line per hold when the host holds any, and carries running_workloads, reserved and reservations under capacity in its JSON.

Not to be confused with

  • The lease — time-bounded ownership of one mutation subject, serializing writers. A reservation serializes nothing; it subtracts.
  • The job claim — a queue job is counted by the agent that runs it, in running_jobs; a reservation is for work the queue does not run.
  • A grant — a credential capability. A reservation confers no authority; a workload with no grant is refused before it could hold anything.

Source: this website