Skip to content

Product1 publisher3 min readPublished

How one shared policy library ran a provisioning service out of memory

A policy library that behaved well when an engineer ran it on one storage instance at a time exhausted a provisioning service's memory once the same calls ran in parallel. Nothing in it was broken.

The Product Desk · Product desk

Illustration accompanying How one shared policy library ran a provisioning service out of memory

What happened

  • A large-scale provisioning system created storage partitions on demand and applied an access-control policy to each new one through a shared library whose other caller was an operator CLI.
  • The provisioning service applied those policies concurrently, so one request could fan out into many simultaneous calls to the library. On ordinary requests that improved throughput.
  • When a much larger request arrived, each parallel operation loaded the state it needed independently and the combined memory footprint grew until the service exhausted its memory limit.
  • The account's proposed fix is to make resource constraints explicit and use signals such as memory pressure, concurrency and queue depth to control how much work the system accepts.

Compiled by The Product DeskSomething wrong?How this is made

Why it matters

  • constraint Reusing a shared component takes a number that sits outside its interface: bytes per call, measured under the new caller's concurrency. Without it, the second caller cannot divide its memory headroom into calls.
  • decision Admission control becomes a sizing decision the team has to make on purpose. Cap request size against a worst-case unit cost, or throttle on live memory pressure. The two put the refusal in different places.
  • exposure The requester who submits the largest valid request is the one who takes the service down, having done nothing wrong and unable to see from outside that the job was oversized for the budget.
  • precedent A library with two callers of different shapes is two products with two budgets, and a serial benchmark that used to be sufficient evidence becomes evidence about only one of them.

Picture the original caller: one command, one storage instance, the current policy state read into memory, the changes computed and applied [2]. The footprint is bounded, and the caller is what bounds it. The number could stay implicit, because a person at a terminal cannot run fifty of those at once.

The provisioning service reused the function and left that assumption behind in the CLI [3][6]. Per-call memory cost stayed the same, and the multiplier went up [1]. The same code path was safe at one call and over the limit at high fan-out, and the interface looked identical either way.

Then the timing. The library had been optimized shortly before the incident, and the tests ran mainly against the original serial pattern [7]. A serial benchmark measures the cost of one call, and the outage came from the sum of many concurrent ones [2].

A second caller adopts the documented interface and also a cost model the interface omits: bytes per call, and how many calls at once are survivable. In this system the size of a provisioning request went unchecked against its expected resource cost, and concurrent work ran untied to the memory pressure those policy operations created [8]. Every part behaved sensibly, including the requester, who submitted a valid request [5].

devops.com put the needed rule as "The provisioning service must not accept or execute more work than it can process within its resource budget" [9]. The publication also concedes the awkward part: a fixed concurrency limit helps, but the safe level moves with request size, state size, traffic and available memory [10]. So the budget has to be an input to the decision, fed by signals such as memory pressure, concurrency and queue depth [11]. In my view that trade is worth taking with eyes open, because on the requester's side the improvement arrives as a job that gets queued or refused for reasons they cannot see from outside.

Two questions sort this for any shared component about to acquire a second caller. Has the per-unit resource cost been measured under the new caller's concurrency? And can a single request contain an unbounded number of units?

Measured cost with a capped request size is the safe case. Measured cost with an uncapped request gives you a divisor: headroom divided by worst-case unit cost is the largest request you can admit. Unmeasured cost with a cap means the cap is a guess that has held so far. Worth knowing before someone raises it for throughput. Unmeasured cost with no cap is the provisioning service, working fine until a request arrives that is large enough to find the memory limit [4].

The account was published without the service's memory limit, the concurrency level, or the size of the request that exhausted it [3]. Anyone applying the shape to their own system will have to measure those numbers there.

What to watch

  • Whether the follow-up publishes the specific counters and thresholds the provisioning service ended up admitting work against.
  • Whether owners of shared internal libraries start publishing a per-call resource cost alongside the API contract.
  • Whether the next incident of this shape involves a caller other than a provisioning service. That would tell you the pattern is about fan-out.
Loading claim ledger
Loading source directory links
Loading share composer
Loading topic controls
Loading related stories