# ADR-009: Bounded-resources budget model ## Status Accepted ## Context Git servers are internet-facing by definition; unbounded loops and buffers are bugs. Each POC surfaced specific unbounded surfaces that need budgets: - Negotiation rounds / haves count (fetch can loop forever without `done`). - receive-pack POST body size (push can be arbitrarily large; alkhttp custom routes get hyper's unbounded stream — POC-3). - Session wall-clock (long-lived ssh/git sessions). - Blocking-pool usage: pack generation runs on `spawn_blocking`; a thundering herd of fetches can starve the pool (POC-2 follow-up 2). - Sideband chunk size is bounded (65000) but max pack size per fetch is still unbounded above it. - alkcall channels carry their own backpressure limits (alkcall ADR-040); git sessions ride raw duplex streams, so those limits do not automatically apply. ## Decision Every session carries a `Limits` value, constructed by the door/adapter from assembler config and handed to the wire layer as part of the session tuple (ADR-002). On the native path the producer adapter is in-crate (`GitAdapter`), but `Limits` still originates from assembler config — the adapter's constructor takes it. Defaults are crate constants; overrides are assembler config. | Budget | Applies to | Default direction | |---|---|---| | max negotiation rounds | fetch (V2, no `done`) | tens | | max haves per round | fetch | thousands (the client's stateless doubling can legitimately reach 16384 — set the default at or above that) | | max pack size | receive-pack | config-bound (tens of MB v1) | | max request body | http POSTs (receive-pack especially) | same as max pack size | | session wall clock | all sessions (enforced by transport's session loop on every door — it is the one component all doors hand the session to; alkcall channel caps add a second bound where channels exist) | tens of minutes | | max concurrent blocking pipeline tasks | server-wide (blocking-pool budget; covers pack generation and pack ingestion alike — both are `spawn_blocking` consumers) | small count | | sideband chunk size | fetch streaming | 65000 (fixed, per protocol) | | max advertisement refs | ls-refs response | config-bound | On breach: the session ends with a substrate-appropriate error — pkt-line error band + close on duplex; on http, *client-fault* budgets (request body size) map to 413, *server/session* budgets (wall clock, rounds, generation concurrency exhaustion) map to 503. Budgets are fail-closed. Max pack size on fetch is *not* budgeted in v1 (the pack is a function of the repo, not the request); receive-pack is the untrusted-input path and gets the hard cap. ## Consequences - No adapter can forget a budget: transport refuses to start a session without `Limits` (part of the tuple, ADR-002). - Streaming stays O(counts) regardless of budgets; budgets bound *aggregate* work, not internal buffering. - The blocking-pool budget is enforced at assembly/acceptance time (reject/slow-path excess concurrent generations), not per-byte. Enforcement point (review 001 A-6): the wire layer — a permit acquired around each `GitPackGen`/`GitPackIngest` call; impls own their internal `spawn_blocking` (backend.md concurrency model). ## References - `docs/research/vision.md` §"Guiding principles" 7 - `docs/research/poc2-findings.md` follow-ups 2–3; `docs/research/poc3-findings.md` follow-up 3 - alkcall ADR-040 (channel backpressure — the thing git sessions bypass) - ADR-002 (session tuple), transport.md §Limits