reservations
A reservation is a tenant's claim on devices of one pool for a time window. `create` first writes the request as a draft, Better IAM decides once on that draft with the request as attributes, and only an allowed draft is charged against quota and placed. A reservation holds its quota charge until it reaches a terminal state, and the `capacity.reservations` reconciler drives it through `placed`, `binding` and `active` to `released`. See [Reservations](/docs/guides/capacity/reservations).
A reservation is a tenant's claim on devices of one pool for a time window. create first writes the request as a draft, Better IAM decides once on that draft with the request as attributes, and only an allowed draft is charged against quota and placed. A reservation holds its quota charge until it reaches a terminal state, and the capacity.reservations reconciler drives it through placed, binding and active to released. See Reservations.
All 7 methods are served by this release.
| Method | Kind | Authorization | Phase | Status |
|---|---|---|---|---|
create | mutation | compute:reservations:create @ compute-reservation | v0.1 | registered |
get | read | compute:reservations:read @ compute-reservation | v0.1 | registered |
list | read | compute:reservations:read @ compute-tenant + items | v0.1 | registered |
release | mutation | compute:reservations:release @ compute-reservation | v0.1 | registered |
cancel | mutation | compute:reservations:release @ compute-reservation | v0.2 | registered |
extend | mutation | compute:reservations:extend @ compute-reservation | v0.1 | registered |
renew | mutation | compute:reservations:extend @ compute-reservation | v0.1 | registered |
create
POST /api/compute/reservations/create
Books devices of one pool for a time window after one IAM decision on the draft; returns the reservation, placed or pending.
- Idempotency. The reservation id is derived from the tenant, the caller's identity and
idempotencyKey. The same key with the same request returns the stored reservation without a second decision once it is past the draft stage, and decides the same draft again while it is still a draft. The same key with a different request isCONFLICT, withdetails.reservationId; a key whose request was refused staysACCESS_DENIED.waitis not part of the compared request. - Decision.
compute:reservations:createis decided on the draft (compute-reservation), with attributes such asgpuModel,gpuCount,durationHoursandregion. When an agent in a delegated session is refused and IAM opens a confirmation request, the call fails withCONFIRMATION_REQUIREDand the draft waits inawaiting-confirmationfor at most 30 minutes; a retry with the same key after approval continues it. - Errors.
NOT_FOUNDwhen the pool is not owned by or offered to the tenant, or is archived.INVALID_INPUTwhen a device class or sharing mode does not match the pool's,binding: 'node'names a pool withoutpolicy.tenantRoot,binding: 'kubernetes'names a pool withoutpolicy.kubernetesor comes from a tenant other than the pool's owner (the kubelet gives bound GPUs to any pod of the cluster, so only the cluster's owner books into it), or a node binding asks forresources.UNSUPPORTEDfor any binding other thanhold,nodeorkubernetes.QUOTA_EXCEEDEDwhen the charge does not fit; the draft stays, so the same key can be retried.SPEND_LIMIT_REACHEDwhen the pool or the tenant's offer asks for a budget check and the budget is spent. - Host resources.
resources(vcpu,memoryGiB,hugepages1Gi, per node for a gang) books host CPU, memory and 1 GiB hugepages on the same node as the devices, and the node enforces them. Without it, a request for whole GPUs books the pool's per-GPU share, stored on the reservation at creation. That share ispolicy.perGpuwhere it is set, or otherwise the smallest share any of the pool's GPU hosts can give (its allocatable CPUs or memory divided by its bookable GPUs). Requests for MIG instances or slots alone, and pools whose hosts report no CPU or memory rows, book none. The share is also the floor:resourcesbelow it (or{}) are raised to it, so a reservation's workload is always limited. The share rides on the GPUs, and needs no quota beyond theirs. Anything booked above it is charged to the tenant'svcpuandmemoryGiBquota; 1 GiB hugepages count as memory. A missing quota row grants 0, so the excess isQUOTA_EXCEEDEDunless the offer names these resources. IAM seesvcpuandmemoryGiBas attributes (summed over a gang). - Placement. A request that does not fit, and is not spot, may preempt spot work. Otherwise it is
pendingwith anUnschedulablecondition and retried every 30 seconds, or, with the pool-lease placer,pendingwith aQueuedcondition.wait.timeoutMs(at most 30 seconds) holds the answer while the reservation is queued; a timeout returns the reservation still queued, not an error.
Emits reservation.placed, reservation.pending, reservation.queued or reservation.awaiting-confirmation.
- Kind: mutation
- Authorization:
compute:reservations:create@compute-reservation - Module:
capacity - Phase: v0.1 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
poolId | string | yes | at most 57 characters; matches ^pool_[0-9a-hjkmnp-tv-z]{26,52}$ |
projectId | string | at most 56 characters; matches ^prj_[0-9a-hjkmnp-tv-z]{26,52}$ | |
devices | list of object | yes | 1-8 items |
devices[].class | one of gpu, gpu.mig, gpu.slot, nic.vf, disk, node | yes | |
devices[].count | integer | yes | 1-64 |
devices[].selector | object | ||
devices[].topology | one of any, nvlink-all | ||
devices[].sharing | one of exclusive, mig, timeslice, mps | ||
topology | one of any, nvlink-all | ||
start | integer | 0-140737488355328 | |
duration | string,integer | yes | |
binding | string | default "hold" | |
resources | object | ||
resources.vcpu | integer | 0-4096 | |
resources.memoryGiB | integer | 0-65536 | |
resources.hugepages1Gi | integer | 0-4096 | |
node | object | ||
node.profileId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:/-]{1,128}$ |
node.profileVersion | integer | at least 1 | |
leaseSeconds | integer | 30-86400 | |
spot | boolean | ||
idempotencyKey | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
wait | object | ||
wait.timeoutMs | integer | yes | 0-30000 |
gang | object | ||
gang.size | integer | yes | 2-64 |
gang.topologyKey | string | at most 128 characters |
get
POST /api/compute/reservations/get
Returns one reservation of the tenant with its operation and, once a node reported its devices bound, its binding.
Drafts are internal: the call is NOT_FOUND for them, as for a reservation that does not exist in the tenant.
- Kind: read
- Authorization:
compute:reservations:read@compute-reservation - Module:
capacity - Phase: v0.1 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
reservationId | string | yes | at most 56 characters; matches ^rsv_[0-9a-hjkmnp-tv-z]{26,52}$ |
list
POST /api/compute/reservations/list
Returns the tenant's reservations newest first, skipping drafts, filtered by state and poolId, one page at a time.
With scope: 'mine' (the default) only the caller's own reservations are listed. With scope: 'all' the tenant's reservations are loaded and each one is checked as compute:reservations:read on the reservation, so only the allowed ones are returned. limit is 1 to 200, default 50; cursor is the id of the last reservation of the previous page, and the answer carries the next cursor while more remain.
- Kind: read
- Authorization:
compute:reservations:read@compute-tenant+ items - Module:
capacity - Phase: v0.1 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
scope | one of mine, all | default "mine" | |
state | one of draft, denied, awaiting-confirmation, pending, placed, binding, active, releasing, released, failed, cancelled, expired | ||
poolId | string | at most 57 characters; matches ^pool_[0-9a-hjkmnp-tv-z]{26,52}$ | |
limit | integer | 1-200 | |
cursor | string | at most 56 characters; matches ^rsv_[0-9a-hjkmnp-tv-z]{26,52}$ |
release
POST /api/compute/reservations/release
Ends a reservation now: its bookings are released at compute_now(), its quota is refunded, and it becomes released; returns the reservation.
A pending reservation is cancelled instead. A reservation already in a terminal state is returned unchanged. A bound binding: 'kubernetes' reservation is released at once, and its node withdraws the devices from the kubelet. Its devices and host resources stay booked, and billed, until the node reports that no pod holds them (AllocationReleased) or the node is lost, and the reservation carries the DevicesHeld condition meanwhile. Until then nothing else can book them. If pods still hold them at the booking's end, the booking is carried on in 5-minute steps while the devices are free, unbilled, for at most 24 hours; the node is KubernetesReleaseStuck (NoSchedule) meanwhile. A booking made for those devices in advance takes them at its start. A request still in draft or awaiting-confirmation is INVALID_STATE. Freed capacity wakes the pool's pending reservations. Emits reservation.released or reservation.cancelled, with reason: 'release'.
- Kind: mutation
- Authorization:
compute:reservations:release@compute-reservation - Module:
capacity - Phase: v0.1 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
reservationId | string | yes | at most 56 characters; matches ^rsv_[0-9a-hjkmnp-tv-z]{26,52}$ |
cancel
POST /api/compute/reservations/cancel
Withdraws a reservation that has not started (pending or placed): its bookings are released, its quota is refunded, and it becomes cancelled; returns the reservation.
A reservation already cancelled is returned unchanged. Any other state is INVALID_STATE; use release for a reservation that has started. Drafts are NOT_FOUND. It is decided as compute:reservations:release. Emits reservation.cancelled.
- Kind: mutation
- Authorization:
compute:reservations:release@compute-reservation - Module:
capacity - Phase: v0.2 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
reservationId | string | yes | at most 56 characters; matches ^rsv_[0-9a-hjkmnp-tv-z]{26,52}$ |
extend
POST /api/compute/reservations/extend
Moves the end of a placed, binding or active reservation later, all or nothing, after one IAM decision; returns the reservation and whether it was extended.
- Give exactly one of
until(a new end, epoch milliseconds) orby(a duration); both or neither isINVALID_INPUT. Other states areINVALID_STATE. - It is admitted like
create: the pending change goes into the reservation's authz row withdurationHoursset to the total after the change, so a policy that caps duration also caps extensions. A refusal isACCESS_DENIEDand clears the pending change.SPEND_LIMIT_REACHEDapplies as forcreate. - When another booking starts before the new end, nothing moves and the answer is
extended: falsewithavailableUntil, the latest end the bookings can reach. - Idempotency. An
untilthe reservation already reaches answersextended: falsewithout a decision, so a retrieduntilis safe.idempotencyKeydoes not replay: a repeatedbyextends again from the new end. - While another extend of the same reservation is being decided, the call is
CONFLICTwithdetails.reason: 'extend-in-flight'andretryAfterSeconds.
Emits reservation.extended with the new end.
- Kind: mutation
- Authorization:
compute:reservations:extend@compute-reservation - Module:
capacity - Phase: v0.1 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
reservationId | string | yes | at most 56 characters; matches ^rsv_[0-9a-hjkmnp-tv-z]{26,52}$ |
until | integer | 0-140737488355328 | |
by | string,integer | ||
idempotencyKey | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
renew
POST /api/compute/reservations/renew
Moves the lease deadline of a renewable reservation to now plus its leaseSeconds, without changing its window; returns the reservation.
A reservation created without leaseSeconds is INVALID_STATE, as is one not in placed, binding or active. A lease that lapses without renewal releases the reservation with reason lease-lapsed. It is decided as compute:reservations:extend. Renewing emits no event.
- Kind: mutation
- Authorization:
compute:reservations:extend@compute-reservation - Module:
capacity - Phase: v0.1 (registered)
Input
| Field | Type | Required | Constraints |
|---|---|---|---|
tenantId | string | yes | at most 128 characters; matches ^[A-Za-z0-9._:-]{1,128}$ |
reservationId | string | yes | at most 56 characters; matches ^rsv_[0-9a-hjkmnp-tv-z]{26,52}$ |
Better Compute is created by Sean Filimon
Last updated
releases
Releases are signed bcd builds the platform may roll out to its nodes. A release is registered from a signed manifest, which must verify under the release key pins given to `rolloutModule`, and rollouts can then target its version. Both methods decide `compute:fleet:admin` on the fleet scope. See [Rollouts](/docs/guides/rollouts).
rollouts
Rollouts take a registered bcd release to the nodes of the tenant's pools in waves, within each pool's disruption budget. A controller settles nodes as they report the new version, pauses the rollout when a wave's failure ratio exceeds its limit, and soaks between waves. Every method decides `compute:fleet:admin` on the fleet scope. See [Rollouts](/docs/guides/rollouts).