1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
//! **One namespace's room, shared by every upload in flight against it.**
//!
//! [`Allowance`] is a number measured off the disk and handed to one transfer.
//! That is correct for one transfer and wrong for two, because both measure the
//! same "before":
//!
//! * The HTTP object endpoint passes `taken = 0` — *"one request being one
//! measurement"* — so every `PUT` is judged against the allowance measured
//! when its own `open` ran.
//! * Two simultaneous SSH sessions each carry their own [`crate::Session`]'s
//! `taken`, which counts that session's objects and knows nothing of the
//! other's.
//!
//! A tenant with 1 GiB left who opens ten simultaneous 1 GiB uploads was
//! admitted for all ten. It is a time-of-check/time-of-use on a resource limit,
//! bounded by N × object size and narrowed — not closed — by the fact that the
//! measuring walk counts in-flight bytes under `lfs/incomplete/`: the bytes have
//! to have LANDED to be counted, and every admission happens before any of them
//! do. `tests/concurrent_allowance.rs` measured it at 2× with eight clients and
//! room for four.
//!
//! # The shape: a monotone counter per namespace, and NOTHING to give back
//!
//! Every admission adds the object's declared size to one `u64` per namespace.
//! The counter only ever goes up. A holder ([`Mark`]) records the value it saw
//! at the moment it was created, and every later admission is judged against the
//! bytes admitted **since that mark**:
//!
//! ```text
//! admitted_since(mark) + this_object <= allowance.bytes
//! ```
//!
//! `allowance.bytes` is already `cap - held`, measured off the disk just after
//! the mark was taken, so the sum is the whole truth: what was on disk, plus
//! everything admitted since, plus what is being asked for now.
//!
//! ## Why the mark is taken BEFORE the disk is walked
//!
//! An admission that lands *during* the walk may or may not be visible to it.
//! Marking first puts such an admission after the mark, so it is charged — and
//! if the walk happened to see its bytes as well, it is charged twice, which
//! refuses slightly early. Marking after the walk would put it before the mark
//! AND possibly outside the walk, which admits too much. Of the two roundings
//! only one of them is a quota that leaks, so the order is not a matter of
//! taste. `gunnar_ssh::server::Dispatch::ask_plan` takes the mark before it asks
//! any gate for an allowance, and that ordering is the contract.
//!
//! ## Why there is no reservation to release, and why that is the point
//!
//! The obvious design is a reservation taken at admit and released on
//! completion or abort, guarded by `Drop`. It is also the design with a
//! failure mode nobody wants: a release that does not run — a panic in the
//! wrong place, a connection dropped where the guard was already forgotten, a
//! `process::abort` — leaks bytes that are charged to the tenant **forever**,
//! and the tenant is locked out of quota they own by a crash. Exactly that
//! class of bug was found in this repository the same week ("a purge that
//! fails still releases the lockout").
//!
//! Nothing is held here, so nothing can be leaked. A holder's charge ages out on
//! its own: the NEXT holder marks at the current value of the counter, so
//! everything charged before it is invisible to it, and what is on disk is what
//! its own walk measured. There is no `Drop` impl in this file. A panic between
//! admit and commit costs the panicking transfer's own charge for the life of
//! that one holder and nothing after it, and
//! `tests/concurrent_allowance.rs::a_panic_between_admit_and_commit_does_not_cost_the_next_upload`
//! is that failure path, run.
//!
//! ## What it costs, honestly
//!
//! An aborted upload stays charged against the holders that were alive when it
//! was admitted. On the HTTP wire a holder is one request, so it clears within
//! milliseconds. On a long-lived SSH session it lasts to the end of that
//! session: that session sees less room than truly free, by at most the bytes
//! of the uploads that failed beside it. **Over-conservative, transient, and
//! self-healing** — the safe direction, and it is a refusal a client retries
//! rather than a byte the operator pays for.
//!
//! The alternative that would remove even that — decrementing on abort — buys a
//! narrower window in exchange for the exact leak-a-reservation hazard above,
//! and the arithmetic that makes a holder's snapshot survive a decrement is a
//! second thing to get right. Not worth it. If the SSH session case ever bites,
//! the cheap fix is a fresh mark per `put-object` rather than per session, which
//! is four lines and no new hazard.
//!
//! # Why the PUSH door borrows this file, and why the file did not move
//!
//! Since 2026-09-08 the pushed-packfile ceiling uses the same counter shape:
//! `gunnar_ssh::server::Dispatch::ask_plan` marks [`AllowanceLedger::shared_pack`]
//! before the gate chain walks anything, `gunnar_server::transport` binds that
//! mark to the number the tightest gate asked for, and `gunnar-wire`'s receive
//! loop charges it per chunk through [`Mark::admit_upto`]. Two doors, one
//! shape, one file.
//!
//! The obvious tidy-up is to move this file to `gunnar-core`, where both doors
//! could name it without an eyebrow. **It would cost the appliance a hundred
//! crates.** `gunnar-lfs` is a LEAF — `gunnar-pktline`, `gix-hash` and nothing
//! else — and `gunnar-ssh`'s manifest depends on it saying so out loud;
//! `gunnar-core` carries the identity table, the control database (redb), the
//! crypto backend, the certificate issuer and the legacy Arrow log. MEASURED
//! 2026-09-08 with `cargo tree -e normal`: `gunnar-lfs` resolves 44 packages,
//! `gunnar-core` 141. Moving the ledger up would drag those 141 into
//! `gunnar-lfs-serve`, whose whole argument is that it is small.
//!
//! `gunnar-wire` — the crate that does the charging — reaches it through
//! neither: it names a one-method trait of its own (`gunnar_wire::PackLedger`)
//! and `gunnar-server`, which already holds both halves of the `PackCeiling`
//! twin, implements it in four lines. That is the dependency edge inverted
//! rather than added, and it is why `gunnar-wire` still depends on exactly what
//! it depended on before (51 packages, unchanged).
//!
//! # Why not serialise uploads per namespace instead
//!
//! Because it would cost more than the bug. One upload at a time per namespace
//! turns a tenant's eight parallel LFS uploads into eight sequential ones — the
//! git-lfs client defaults to three concurrent transfers and people raise it —
//! and the overshoot it buys back is at most one object's worth over what this
//! already gives. The lock here is held across a hash lookup and an integer add,
//! never across the disk walk, never across the transfer. Two uploads still run
//! at full speed; only their bookkeeping is ordered.
use HashMap;
use ;
use crateAllowance;
use crate;
/// **Bytes admitted per namespace, monotonically.**
///
/// One entry per namespace the process has ever admitted an upload for, and
/// entries are never removed: the map is bounded by the size of the catalog,
/// each entry is a namespace name and eight bytes, and removing them would mean
/// counting live holders, which is the reservation bookkeeping this design
/// exists to avoid. A box with ten thousand namespaces spends well under a
/// megabyte on it, once.
/// The one ledger the whole process shares **for `lfs_bytes`**.
///
/// A `static` rather than a field on `Control` or `Dispatch`, and the reason is
/// the property being defended: a namespace's room is a fact about the BOX, and
/// two doors that shared a tenant but not a ledger would be exactly the bug
/// again, one wire over. `gunnar-server` builds a `Dispatch` on more than one
/// path, and nothing in the type system would have caught a second one holding
/// a second ledger. Tests that want isolation build their own with
/// [`AllowanceLedger::detached`].
static SHARED: = new;
/// The same, for `pack_bytes`. See [`AllowanceLedger::shared_pack`] for why
/// this is a second counter and not the same one.
static SHARED_PACK: = new;
/// **Where one transfer's holder stands in the namespace's ledger** — the value
/// the counter had when this transfer measured the disk.
///
/// `Clone` is a real clone of the same standing, not a new one: the request, the
/// [`crate::Session`] built from it and `LfsAccess`'s copy are one transfer that
/// happens to be held in three places, and giving a clone a fresh mark would
/// hide every byte charged in between from it.
/// **An [`Allowance`] that knows what else is in flight against the same
/// namespace.**
///
/// Everything the plain [`Allowance`] is — a byte count and the sentence a
/// tenant who runs out reads — plus the one thing it could not have: the other
/// uploads. `gunnar-server` still computes the number, this still carries it to
/// where the bytes are, and the only new fact is which namespace's room it is a
/// slice of.