✨ Add API/CLI to reset bucket usage for an entity/resource pair
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 0
- Forks
- 0
- Avg merge
- 6h 51m
- Merged PRs (30d)
- 104
Description
[!NOTE]
Parked pending the scheduled-limits ADR tracked in #222 — moved to the v1.0.0 milestone.
This is a sequencing decision, not a rejection of the work.
reset_bucket()was requested as a primitive for an out-of-tree cron job that swaps an
entity/resource's capacity at each tick. #222 is to make the library do time-based scheduling
natively, which may remove the need for this entirely — a calendar-aligned refill supersedes
"reset usage at a boundary". Decide this issue only after the #222 ADR is accepted: it may
be closed as unnecessary, or narrowed.Design objections already on record, kept here so they are not lost:
- Deleting bucket items throws away
total_consumed_milli(the counter the aggregator diffs to
produce usage snapshots — see #179), the ADR-125disabledstamp, andshard_count.- In-flight
adjust()/rollback writes are unconditionalADDs, so one landing after the delete
recreates a skeleton item that lackscp/ra/rp.- An in-place
SET b_{l}_tk = :effective_cp, rf = :nowper shard avoids all of the above at the
same cost, and is the shape any narrowed version should take — so the "Mechanism: delete, don't
in-place update" section below is the part most likely to change.
Problem or Use Case
There's no way to clear a single entity's accumulated usage state for one resource. Today the only way to change how much of a bucket is consumed is to wait for lazy refill, or to call lease.adjust() — which can only nudge the bucket, not reset it to a clean state.
The concrete case this blocks: an operator tightens or corrects a misconfigured limit (set_resource_defaults() / set_limits()), but the entity's existing bucket item still carries the old b_{limit}_cp/ra/rp values baked in at bucket-creation time (see build_composite_create in repository.py), plus whatever debt/consumption already accumulated under the old config. Support wants to give that one entity a blank slate — e.g. after fixing a bug that over-consumed tokens, or after raising an entity's limit and wanting it to take effect immediately rather than after a long refill period — without resetting every other entity on the resource (ruling out disable_resource/enable_resource, which are namespace/resource-wide) and without deleting and recreating the entity itself (which would lose its config and audit history).
Proposed Solution
Add Repository.reset_bucket(entity_id, resource, principal=None) -> int (Async; SyncRepository.reset_bucket(...) generated per ADR-121), mirroring the Repository-level placement of disable_entity()/enable_entity() (ADR-125) rather than RateLimiter — this is an admin action, not runtime limiting.
from zae_limiter import Repository
repo = await Repository.open("my-app")
# Wipe accumulated usage for one entity+resource, all shards.
count = await repo.reset_bucket("user-123", "gpt-4")
# count == number of bucket shard items deleted (0 if none existed)
CLI, following the existing entity disable/entity enable/entity clear-disabled command shape:
zae-limiter entity reset-bucket ENTITY_ID --resource RESOURCE [--namespace NS]
Mechanism: delete, don't in-place update
reset_bucket deletes the composite bucket item(s) for (entity_id, resource) rather than issuing UpdateItem SET tk = cp on each limit:
- Discover shards via a resource-scoped GSI3 query —
GSI3PK={ns}/ENTITY#{id}, GSI3SK begins_with BUCKET#{resource}#— not the unscopedget_buckets()discovery query, since we only want this resource's shards, not every resource the entity has a bucket for. DeleteItemeach shard'sPK={ns}/BUCKET#{id}#{resource}#{shard}, SK=#STATE(unconditional — a concurrent write to a bucket we're deleting is fine; the entity just gets one more shard-worth of consumption that also gets wiped).- Return the count of items deleted.
Deleting (rather than resetting tk in place) means the next acquire() recreates the bucket via the normal slow path with whatever limits are configured now (picking up any interim set_limits()/set_resource_defaults() change for free) and with shard_count collapsed back to 1 — instead of leaving stale cp/ra/rp values or an inflated shard_count from prior write-sharding (GHSA-76rv-2r9v-c5m6) in place.
Why this doesn't bypass disabled (ADR-125)
Deleting the bucket item also removes any disabled stamp on it. This is safe: a deleted bucket forces the next acquire() onto the slow path (speculative_consume() requires attribute_exists(PK); a missing item always falls back per the "Speculative Writes" invariant), and the slow path calls resolve_disabled() before creating a new bucket. So an entity/resource disabled via disable_entity()/disable_resource() cannot be re-admitted by calling reset_bucket().
Edge cases
| Case | Behavior |
|---|---|
No bucket exists for (entity_id, resource) |
No-op, returns 0. Not an error — matches enable_entity()/clear_resource_disabled()'s no-op-on-missing precedent. |
Bucket was write-sharded (shard_count > 1) |
All shards discovered and deleted, not just shard 0. |
Entity has cascade=True |
Only the child's bucket for resource is reset. The parent's bucket (and any other resource) is untouched — cascade is out of scope for entity-scoped operations, same precedent as ADR-125's entity-level disabled. |
resource never had any limits configured (bucket never created) |
Same as "no bucket exists" — 0, no validation error beyond validate_resource(). |
Alternatives Considered
- In-place
UpdateItem(SET b_{limit}_tk = b_{limit}_cp) per limit — rejected: requires already knowing every limit name on the item (an extra read), doesn't collapse a doubledshard_count, and leaves stalecp/ra/rpin place if limits changed since bucket creation. Delete-and-let-slow-path-recreate is a single unconditional op per shard and self-corrects all of the above. - Reset only the
total_consumed_milliaggregator-refill counter, leavetkuntouched — rejected: doesn't address the actual ask (clearing consumed/negative tokens), only affects the aggregator's refill-decision accounting (Issue #317).
Acceptance Criteria
-
Repository.reset_bucket(entity_id, resource, principal=None) -> intdeletes every shard item for(entity_id, resource), discovered via a GSI3 query scoped withGSI3SK begins_with BUCKET#{resource}#(not an unscoped full-entity bucket discovery). -
SyncRepository.reset_bucket(...)exists insync_repository.pyand is generated (not hand-edited) from the async source per ADR-121;hatch run generate-syncproduces no diff. - Returns
0and raises no exception when no bucket item exists for the pair. - Unit test (moto,
tests/unit/test_repository.py+ generatedtests/unit/test_sync_repository.py) covers: no-op on missing bucket, deletion of all shards whenshard_count > 1, and that a cascading child's reset does not delete or modify the parent's bucket item. - Integration test (LocalStack) verifies
acquire()afterreset_bucket()observes full capacity rather than the pre-reset consumption/debt. -
zae-limiter entity reset-bucket ENTITY_ID --resource RESOURCE [--namespace NS]CLI command added incli.py, following the existingentity disable/entity enablecommand structure, and prints the number of items reset. - A new
AuditActionconstant (e.g.BUCKET_RESET) is logged withentity_idandresourceon every non-zero reset, consistent with existing audit logging for entity/limit mutations. - CLAUDE.md's "DynamoDB Access Patterns" table and "API methods for managing stored limits" section are updated to document the new method and CLI command.
-
docs/cli.mdanddocs/api/reference the new command/method (viadocs-updateragent per docs-parity rule).
Dependencies
None — builds on existing GSI3 bucket-discovery (GHSA-76rv-2r9v-c5m6) and the Repository-level admin-action placement established by ADR-125 (disable_entity/enable_entity).
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
First read the scheduled-limits ADR in #222 and confirm whether this work remains needed. Then inspect build_composite_create in repository.py, the existing entity commands in cli.py, and the repository and sync-repository tests. Done means the accepted scope is implemented across API, generated sync API, CLI, audit logging, tests, and the named documentation, with hatch run generate-sync producing no diff.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- aws, python
- Domain
- backend, cli, databases, documentation, testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100