ADR-149: Reset and top-up are in-place, rf-locked passes over every shard¶
Status: Accepted Date: 2026-10-09 Issue: #470 Amends: ADR-145 (I1, I7, I8: a top-up is an operator grant above the plan), ADR-139 (a session top-up with no live window opens one at the top-up instant: an operator action, not an admitted request, anchors it) Related: ADR-125, ADR-137, ADR-139, ADR-140, ADR-141, ADR-145, ADR-147, #471, #674
Context¶
The #674 use case needs a plan upgrade, a purchase of more allowance, or a session reset to
take effect immediately for one (entity, resource). Today nothing does that: set_limits()
changes a bucket's parameters but never adds tokens, so an upgraded quota (which does not drip,
ADR-137) keeps its old remaining balance until the next reset, and a purchase has no
representation at all.
PR #471 resets by deleting the bucket items. That loses the consumption counter the aggregator
diffs (#179), the disabled stamp, shard_count and the ADR-145 grant record, and an in-flight
adjust() (an unconditional ADD) recreates a skeleton item with no parameters.
Three facts about the current code constrain an in-place write. Every refill clamps a balance to
its ceiling, which for a quota is C // gc, so a credit above the ceiling is trimmed by the next
pass of any writer. rf is one attribute per item, so a write that moves it without materialising
every limit on the item skips their refill and any pending reset edge. And a sharded quota holds
its allowance in slices whose sum ADR-145 conserves, so a credit to one shard is a mint unless it
is counted.
Decision¶
Reset and top-up of an (entity, resource) must be in-place, rf-locked passes over every
existing shard in one transaction, recorded by one operation record per call. A quota may be
topped up above its ceiling only through b_{q}_tu, which ends with the period and is gated at
0.17.0.
Consequences¶
Positive:
- Plan upgrades, purchases and session resets take effect on the next request, on every shard.
- Each shard write is a zero-consumption slow-path pass: every limit materialised, the named
ones changed by a token delta (never a set), tc, disabled and the parent untouched, and
the shard's next acquire forced through one materialising pass. Nothing the item carries is
lost; the fast path stays 0 RCU + 1 WCU and reads nothing new.
- The operation record (put only if absent, in the same transaction) lets exactly one write per
id commit and resolves an unclear failure; a caller may supply the id as an idempotency key.
- A reset starts a new period: a quota's grant is recorded at the planned count; a session
window is marked ended and applied, its balance left for the next opener to restore.
- A quota top-up adds exactly N. A dripping limit's is capped at its ceiling: it has no period
to bound extra headroom, so raising its rate is set_limits' job.
- ADR-145 is amended, not replaced: a top-up is an operator grant above the plan, so I1 gains
it as a source, I7's ceiling becomes C // gc + tu, and I8 reads admitted plus held plus
still-grantable equals the capacity plus the top-ups granted.
- The aggregator and concurrent slow paths are excluded by the rf lock they already honour;
older clients and aggregators, which would clamp tu away, by the ADR-141 gate.
- Imperative only (no manifest or CloudFormation surface); audited; this process's rejection
cache (ADR-147) is cleared. Details and costs: docs/plans/2026-10-09-reset-and-top-up-design.md.
Negative:
- Cost is O(shards) reads and two write units per shard, plus two for the operation record
and two for the audit event, which ride in the same transaction so a grant and its audit
commit together: even a one-shard operation is a three-item transaction (6 WRU where one
UpdateItem and a separate audit PutItem were 2), against one delete per shard in #471.
- Without an idempotency key, an error from a reset or top-up leaves its outcome unknown to the
caller; the operation record lives three days.
- reset_bucket / top_up are required RepositoryProtocol members, not capability-gated (ADR-109), and their audit event rides in the same transaction, so a backend that implements them must also write audit events (ADR-108 lists audit as optional elsewhere). A top-up that creates the bucket takes its bypass and soft stamps from config read uncached (ADR-151 decision 4).
- A record is tied to the entity's incarnation (#716's pinc), read before the write rather
than checked inside it: an entity deleted and recreated during one call can see a key retry
applied once more. Closing it would put a ConditionCheck on META in every reset and
top-up (+2 WCU a call); v0.17.0 grants dynamodb:ConditionCheckItem (ADR-150), but its
check also pins the parent generation, so every set_parent would cancel resets in flight.
Left open. A top-up that creates the bucket already carries ADR-150's check.
- After a plan upgrade a quota's ceiling is at most one top-up wider than the entitlement until
the next reset; it under-clamps refunds, never mints.
- A conflicting writer on any shard aborts the whole transaction; the operation retries a
bounded number of times and then fails.
- A topped-up balance can sit on shards a request does not draw, so a draw can reject while
the entity holds purchased tokens elsewhere, as ADR-145 already accepts for quotas.
- Another process's rejection cache can refuse for up to its TTL after a reset elsewhere.
- Reset and top-up are operator actions by contract. IAM cannot stop a principal that may
acquire from writing its own buckets, so an application can grant itself allowance; a policy
can only keep it out of the library's calls, not off the items.
- Every reset and roll writer, client and aggregator, gains one attribute to clear, and ADR-145's
conservation fuzz test and writer registry gain a term.
- A credit landing between the read and the write can lift a balance above its target until the
forced materialising pass clamps it.
Alternatives Considered¶
- Delete the bucket items (#471): loses the consumption counter,
disabled,shard_countand the grant record, and in-flight adjustments recreate a corrupt item. - Set the balance without materialising the item: moving
rfskips other limits' refill and pending reset edges; not moving it lets a stale aggregator refill land on top. - Top-up capped at the ceiling only: cannot express a purchase beyond the plan.
- Purchase as a raised configured capacity: persists into later periods and needs a second admin write to undo.
- Independent per-shard writes: a partial reset leaves overlapping quota grants, which over-admits.