feat(quota): add sharded durable reservation protocol (#8402)

* feat(quota): add sharded durable reservation protocol

Co-Authored-By: heihutu <heihutu@gmail.com>

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* fix(quota): preserve typed task error context

Co-Authored-By: heihutu <heihutu@gmail.com>

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* fix(quota): initialize all durable reservation shards

Co-Authored-By: heihutu <heihutu@gmail.com>

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* fix(quota): persist bootstrap shard eligibility

Co-Authored-By: heihutu <heihutu@gmail.com>

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* perf(quota): reuse allocator bootstrap state

Co-Authored-By: heihutu <heihutu@gmail.com>

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* test(connect): widen inventory startup wait

Co-Authored-By: heihutu <heihutu@gmail.com>

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* test(quota): reserve protocol v3 for unknown fixtures

Co-Authored-By: heihutu <heihutu@gmail.com>

Co-Authored-By: zhi22915 <qiuzgang@gmail.com>

* fix(quota): reload allocator after concurrent credit refills

---------

Co-authored-by: zhi22915 <qiuzgang@gmail.com>
Co-authored-by: overtrue <anzhengchao@gmail.com>
This commit is contained in:
Hauser
2026-10-08 16:58:32 +08:00
committed by GitHub
parent bdd23da034
commit bf47a919ac
10 changed files with 1414 additions and 26 deletions
@@ -0,0 +1,79 @@
# Durable quota reservation protocol v2
Protocol v2 is an opt-in hard-quota protocol for distributed deployments. It
keeps the quota limit authoritative in one allocator ledger while moving
request reservations into sixteen independently locked shard ledgers.
## State and invariants
The allocator ledger contains the bucket incarnation, quota revision, limit,
monotonic generation, and durable credit grants. A grant belongs to exactly
one shard and is never larger than the remaining quota at the allocator write
that issued it. A shard ledger contains the same incarnation/revision, the
allocator generation it adopted, its grants, accounted usage, and pending
reservations.
The safety invariant is:
```text
sum(shard grant amounts) <= quota_limit
shard.accounted_usage <= sum(shard grant amounts)
```
`accounted_usage` includes committed bytes and pending positive reservation
growth. An abort removes the pending growth; a commit removes only the pending
record because its growth is already accounted. A reservation whose commit
marker was persisted is removed only as a precharged reconcile candidate; the
shard is then rebuilt from objects belonging to that shard.
## Locking and fencing
The v2 lock order is:
```text
caller object/upload -> metadata transaction -> operation lock -> allocator
read lock -> shard write lock
```
Credit refill releases the operation and shard locks before taking the
allocator write lock. The allocator write is fenced before persistence. Shard
reservation and settlement fence the shard before persistence. The allocator
generation and bucket incarnation/revision are checked on every load, so a
late writer from an older epoch fails closed. Allocator and shard paths include
the bucket incarnation and quota revision, so a quota change starts a new epoch
after an exact bucket bootstrap instead of reusing old grants.
## Crash and replay behavior
Allocator grants are durable before a shard can adopt them. If a process exits
after the allocator write but before the shard write, the next shard writer
adopts the grant by ID. If a process exits after a shard reservation, the
operation lock probe can reclaim reservations and marks `commit_started`
entries for exact shard reconciliation, preventing a crash window from
releasing credit for an object that may already be durable.
Repeated commit/abort operations are keyed by the operation ID and expected
reservation fields; mismatches fail closed.
## Mixed-version rollout
`reservation_protocol: 2` is serialized with `reservation_quota`. A node that
does not understand v2 must reject the configuration rather than treating it
as the legacy snapshot protocol. Operators must upgrade the whole fleet before
calling `BucketQuota::new_sharded`. Downgrades require clearing or reconciling
all v2 allocator/shard ledgers first; v1 and v2 ledgers are never mixed.
The default `BucketQuota::new` remains protocol v1. This keeps existing
deployments compatible until an operator has completed the rollout and chosen
the sharded protocol explicitly.
## Performance boundary
Normal v2 requests take a shared allocator read lock and one shard write lock;
the bucket-wide allocator write lock is used only to issue a new credit chunk.
The per-grant chunk is bounded by roughly one sixteenth of the quota (or the
requested growth when larger), so one active shard cannot consume the entire
remaining quota through a single speculative grant. It is a conservative upper
bound, not a best-effort token bucket.
Unused credit is intentionally retained until the quota epoch changes or a
future reconciliation operation returns it, trading some capacity utilization
for a simple crash-safe upper bound.