Blog ·
The only real limit on rack occupancy
Two engineers pick the same rack unit at the same time. The form says free to both of them. What decides is a partial unique index, and nothing else can.
By Provibr
Two engineers are placing machines in the same rack. Both open the form, both look at U nine, both see a free slot, both press save. Whatever your application code does between those two requests, there is a window in which both of them read the same answer, and no amount of checking before the write closes it.
This is the oldest race in the book and it has one correct answer: the database decides. Everything above it is there to produce a readable refusal, not to prevent a collision. Getting that order the wrong way round is exactly how you end up with a uniqueness rule that leaks on a busy day, and in a datacenter a leak means somebody with a trolley standing in front of a rack that is already full.
The boundary is one index
Occupancy is stored twice on purpose. A claim has a header row, which is the thing somebody made: this face, from this unit to that unit, and for a placement the machine that stands in it. And it has one row per occupied slot: this rack, this face, this single U.
The slot table exists for exactly one reason. It carries a partial unique index on the rack, the face and the slot number, restricted to rows that have not been released. That index is the only real limit on occupancy in the entire system. Nothing else can be, because a unique index cannot join: it can only see the columns of the row it sits on, so the rack and the face have to be denormalised onto the slot row for the constraint to exist at all.
The price is that releasing a claim is two updates inside one transaction, since the released timestamp lives on both the header and the slots. The return is that the invariant is enforced by the database rather than by the care of whoever writes the next feature.
We measured this rather than reasoning about it. Two real Postgres connections, a mutual barrier so that both are inside their transaction before either commits, both claiming front U nine. Result: exactly one claim written, one error 23505 naming that index, and no orphaned header left behind by the rollback. That is the sentence you want to be able to write about a concurrency guarantee, and you cannot write it from reading the code.
What the check before the write is actually for
There is a pure function that plans a claim, and it refuses in three ways, in this order.
The face does not exist. An open frame with no rear is not a rack you hang something in the back of, which is why whether a rack supports a front and a rear is a property of the rack rather than an assumption.
Out of bounds. The whole range has to fit between the starting unit of the rack and its end, inclusive. Racks do not all start at one and do not all number upward, so this is arithmetic against the geometry of that particular rack and not against the number forty two.
Occupied, and this refusal carries the list of slots that clash. A message that says "full" sends somebody back to the form with no idea where to look. A message that says front U nine and U ten are taken sends them to the answer.
None of these is the boundary. Both engineers in the opening paragraph pass all three. The function exists so that the ordinary case, which is one person at a time getting it wrong, produces a sentence instead of a database error code.
Unknown depth behaves like full
Every asset has a depth class: full, half, or unknown. A half depth item claims only the face it hangs on. A full depth item claims front and rear.
Unknown claims front and rear as well, and that choice is worth defending because the intuitive default is the opposite. Unknown feels like missing data that you will fill in later, and while you wait it seems harmless to leave the other side free.
It is not harmless. A machine whose depth turns out to be full cannot be installed if somebody has filled the rear in the meantime, and the correction happens with the machine already on site. Blocking the rear costs you a slot you might have had. Not blocking it costs you a delivery. Unknown is not an absence of a constraint, it is a constraint you cannot yet name, and the safe side of it is the conservative one.
The same principle runs through the rest of the capacity model: an unknown weight, an unknown power draw or a stale claim state reads as unknown or blocked, and never as free capacity.
A placement never expires
Claims come in four kinds. A placement is a machine that is physically there. A blockout is space taken out of service for cabling. A move hold and a reservation are the two that keep space for something in motion or on order. Only the first has a screen today; the other three exist as data so that the primitive is one thing rather than four similar things.
A database check enforces that a placement has no expiry. It sounds like a small rule. What it prevents is a row that quietly stops counting on a Tuesday while the machine is still bolted in, which is to say a rack that the software believes has a free slot in it. An expiring reservation is a feature; an expiring placement is a lie with a timer on it.
A second check requires that a placement has an asset, since the other three kinds legitimately have none, and a partial unique index allows at most one active placement per asset. Without that index the same machine can be recorded in two racks at once. That is not a data quality issue you notice in a report; it is an issue you notice when somebody is standing at the wrong rack.
Worth knowing about indexes of this shape: they are global rather than scoped per organisation. A machine that already carries an active row collides with the unique constraint rather than with an ownership check, even when the second attempt comes from a different organisation. That is the safe side, and it also means a two organisation test needs a third asset, because the constraint fires before the ownership logic ever runs.
The same shape one level up
Handing a machine to a customer has the identical structure, and it needs two indexes rather than one, because it is two invariants that people habitually state as one sentence.
One service is on at most one machine. Without that, a service can point at two assets and its location card points in two directions.
One machine is issued to at most one service. Without that, the same machine gets sold twice, and you find out when two customers call.
Both are partial unique indexes on rows whose end date is still empty, which is the same construction as the slot index and for the same reason: the check in the application produces the readable refusal, the index decides who wins.
One rule could not go into the database. A binding must point at a dedicated service, and a check constraint cannot read another table. That one lives in the function that writes the binding, and it is worth being explicit about which of your rules are enforced by the schema and which are enforced by a code path, because the second kind is only as good as the number of write paths you remember to route through it.
Bindings are historical rather than current. The end date is what makes the row inactive, and every read of a physical location filters on it and on the organisation of both the binding and the asset. Drop those filters and a former customer reads the rack and unit of the customer who came after them. That was proved red before it was fixed, which is the only way to know a filter is doing work.
What we would write down
- Put the uniqueness where the row is. If the constraint needs a join, denormalise the columns it needs; an index cannot join and a trigger is not a constraint.
- Materialise the unit of contention. One row per occupied slot, not a range on a header, because a range cannot be unique against another range.
- Prove the race with two connections and a barrier. Reasoning about isolation levels is not evidence.
- Let the precheck be a message, not a defence, and make it name the exact slots that clash.
- Make unknown behave like the expensive case, so a missing measurement never presents as free space.
For clarity about where this sits: the datacenter logistics work is behind a rollout flag that is off by default, with a per organisation setting on top of it, so none of the above is purchasable today. Two things are unproven: the concurrency measurement was run against blockout claims in a direct database test rather than against the placement path itself, and the move hold and reservation kinds have no writer yet. The wider habit of letting the database be the arbiter shows up elsewhere in this product too, in how addresses are handed out of a pool and in how a vmid range is really a permission boundary.
From the datacenter work on Provibr. Why the five capacity questions above a rack must not be added together is in rack space is not capacity, and what happens when an external system claims to know where a machine stands is in when NetBox is the source of truth.