Qirava Cloud docs
Placement: first-fit bin-packing
When you provision a tenant, the control plane must decide which bare-metal node it lands on. It does this with the simplest correct policy: first-fit bin-packing over the recorded free headroom of each online node. There is no live hypervisor call — placement reasons purely over the _cp_nodes allocation rows, which makes it deterministic and testable. This page explains the placement algorithm, how allocation is tracked and released, what happens when the fleet is full (the pending_capacity state), the seeded nodes the placer packs against, and what is still planned (rebalance and migration).
The algorithm: first-fit by free headroom#
Placement scans the nodes in a stable order (sorted by id, so placement is deterministic and testable), skips any node that is not online, and computes each node's free headroom as cap_* − alloc_* for threads, memory, and storage. The first node whose free headroom satisfies ALL THREE requested dimensions wins. If no node fits, placement returns nothing and the tenant is recorded unplaced (see pending_capacity below). This is SIMULATED: it reasons over the recorded allocation; it does not talk to a hypervisor.
fn place(db, threads, mem_gb, storage_gb) -> Option<String> {
let mut nodes = all_rows(db, CP_NODES);
nodes.sort_by(|a, b| s(a,"id").cmp(&s(b,"id"))); // stable order
for n in &nodes {
if s(n,"status") != "online" { continue; }
let free_thr = i(n,"cap_threads") - i(n,"alloc_threads");
let free_mem = i(n,"cap_mem_gb") - i(n,"alloc_mem_gb");
let free_sto = i(n,"cap_storage_gb")- i(n,"alloc_storage_gb");
if free_thr >= threads && free_mem >= mem_gb && free_sto >= storage_gb {
return Some(s(n,"id")); // first fit wins
}
}
None
}request: 2 thr · 4 GB · 20 GB
scan id-sorted, online only:
node-eu-1 free = 64thr · 256GB · 8000GB → fits ALL three ✓ PLACE HERE
node-eu-2 (not reached — first fit already won)
if neither fit: return None → tenant status = pending_capacityThe seeded nodes#
On first boot the control plane seeds two simulated bare-metal nodes for the placer to bin-pack against. These model the headroom the console reasons about; a real deployment registers actual OVH/Hetzner nodes instead.
| id | provider | cap_threads | cap_mem_gb | cap_storage_gb | status |
|---|---|---|---|---|---|
| node-eu-1 | hetzner | 64 | 256 | 8000 | online |
| node-eu-2 | ovh | 32 | 128 | 4000 | online |
Allocation: reserved on place, released on terminate#
When a tenant is placed, the chosen node's alloc_* counters are bumped by the requested cap × replicas. Every action that changes a tenant's footprint applies a signed delta to the same counters (clamped at 0): vertical scale applies the cap delta × replicas; horizontal scale and switch-mode apply the per-instance cap × the replica delta; terminate subtracts the full cap × replicas. This keeps each node's free headroom accurate for the next placement decision.
// node_alloc_delta: signed delta, clamped at 0, written back via execute
UPDATE _cp_nodes SET alloc_threads = <max(0, old+d)>,
alloc_mem_gb = <…>, alloc_storage_gb = <…>
WHERE id = <node_id>Request
-- provision acme: 2 thr / 4 GB / 20 GB, standalone (replicas 1)
cloud.provision { id: "acme", threads: 2, mem_gb: 4, storage_gb: 20 }
-- then switch to cluster (replicas 2)
cloud.switch_mode { id: "acme", mode: "cluster" }
-- then terminate
cloud.terminate { id: "acme" }Response
-- node-eu-1.alloc_threads over time:
after provision : 2 (2 thr × 1 replica)
after switch : 4 (2 thr × 2 replicas)
after terminate : 0 (released)Output
SELECT alloc_threads FROM _cp_nodes WHERE id = "node-eu-1" -- 0 after terminateWhen the fleet is full: pending_capacity#
If place finds no node with headroom for all three dimensions, the tenant is still recorded — with status = "pending_capacity" and an empty node_id — and the response flags it. In a real deployment the placer would request a new bare-metal node and defer the tenant DMS boot. A later cloud.resume re-evaluates: a tenant with no node returns to pending_capacity (the placer would retry); one with a node returns to active.
Request
cloud.provision { id: "big", threads: 256, mem_gb: 1024, storage_gb: 100000 }
-- (exceeds every node's headroom)Response
{
tenant_id: "big", status: "pending_capacity", node_id: "",
simulated: "PLANNED: no node headroom — placer would request a new bare-metal node; tenant DMS boot deferred"
}What is planned: rebalance and migration#
Today placement happens once, at provision time; there is NO automatic rebalance. The Nodes & Capacity screen states this honestly: placement is first-fit bin-packing by cap headroom over the node rows, and provisioning/scaling updates a node's allocation here — but real node provisioning (OVH/Hetzner), CVM/SEV-SNP setup, and live rebalance/migration are PLANNED.