feat(core): add UsageLimiter::acquire_concurrency_only (PSA-42) #11

Merged
nrupard merged 1 commit from feat/psa-42-acquire-concurrency-only into main 2026-06-03 20:37:17 +02:00
Owner

Problem

UsageLimiter::acquire atomically bumps the in-process concurrency map AND the durable daily counter; there was no way to take a concurrency slot without counting toward the daily cap. bunyip BUNYIP-43 needs to meter only logical pulls (tag-addressed manifest requests) while still concurrency-bounding the digest-addressed follow-up fetches. Because the only API coupled both, bunyip gated the whole acquire behind "is this a tag request", so digest-addressed manifest fetches took no concurrency slot at all - and a multi-arch docker pull (one tag request, then N by-digest platform-manifest fetches) left those N unbounded, defeating concurrent_manifests_per_user for the request type a multi-arch pull emits most of.

Change

Add acquire_concurrency_only(user_id) -> Result<UsageGuard, LimitDenial>:

  • Takes a concurrency slot only: no daily branch, no store call. Synchronous; cannot fail with AppError. The only denial is LimitDenial::Concurrency.
  • Shares the same per-user concurrency budget as acquire, so a consumer can hold a slot for every request but count daily only for the ones representing a logical operation.

The coupled acquire is unchanged (additive API). Both now take the slot through a shared try_take_concurrency_slot helper; acquire's rollback paths release via guard drop, preserving the existing "release the slot before any store call" ordering.

Acceptance criteria

  • A consumer can hold a concurrency slot for a request without incrementing the daily counter.
  • Existing acquire callers are unaffected (additive; signature unchanged).

Tests

  • acquire_concurrency_only_does_not_count_daily: 10 concurrency-only acquisitions leave the daily counter at 0 and never hit the cap; a later coupled acquire still succeeds.
  • acquire_concurrency_only_enforces_concurrency: per-user cap enforced; slot freed on drop.
  • concurrency_only_slot_shares_budget_with_coupled_acquire: a held concurrency-only slot makes a coupled acquire for the same user return Concurrency, without counting daily.

fmt + clippy -D warnings clean; full cargo test --workspace green. The method is inherited by the vertical re-exports (OciLimiter, DownloadLimiter) automatically.

Follow-up (separate)

bunyip switches its digest-addressed manifest fetches to acquire_concurrency_only; depends on a dunite-core release + bunyip dep bump.

#PSA-42

## Problem `UsageLimiter::acquire` atomically bumps the in-process concurrency map AND the durable daily counter; there was no way to take a concurrency slot without counting toward the daily cap. bunyip BUNYIP-43 needs to meter only logical pulls (tag-addressed manifest requests) while still concurrency-bounding the digest-addressed follow-up fetches. Because the only API coupled both, bunyip gated the whole `acquire` behind "is this a tag request", so digest-addressed manifest fetches took no concurrency slot at all - and a multi-arch `docker pull` (one tag request, then N by-digest platform-manifest fetches) left those N unbounded, defeating `concurrent_manifests_per_user` for the request type a multi-arch pull emits most of. ## Change Add `acquire_concurrency_only(user_id) -> Result<UsageGuard, LimitDenial>`: - Takes a concurrency slot only: no daily branch, no store call. Synchronous; cannot fail with `AppError`. The only denial is `LimitDenial::Concurrency`. - Shares the same per-user concurrency budget as `acquire`, so a consumer can hold a slot for every request but count daily only for the ones representing a logical operation. The coupled `acquire` is unchanged (additive API). Both now take the slot through a shared `try_take_concurrency_slot` helper; `acquire`'s rollback paths release via guard drop, preserving the existing "release the slot before any store call" ordering. ## Acceptance criteria - [x] A consumer can hold a concurrency slot for a request without incrementing the daily counter. - [x] Existing `acquire` callers are unaffected (additive; signature unchanged). ## Tests - `acquire_concurrency_only_does_not_count_daily`: 10 concurrency-only acquisitions leave the daily counter at 0 and never hit the cap; a later coupled `acquire` still succeeds. - `acquire_concurrency_only_enforces_concurrency`: per-user cap enforced; slot freed on drop. - `concurrency_only_slot_shares_budget_with_coupled_acquire`: a held concurrency-only slot makes a coupled `acquire` for the same user return `Concurrency`, without counting daily. fmt + clippy -D warnings clean; full `cargo test --workspace` green. The method is inherited by the vertical re-exports (`OciLimiter`, `DownloadLimiter`) automatically. ## Follow-up (separate) bunyip switches its digest-addressed manifest fetches to `acquire_concurrency_only`; depends on a dunite-core release + bunyip dep bump. #PSA-42
feat(core): add UsageLimiter::acquire_concurrency_only (PSA-42)
All checks were successful
Checks / fmt + clippy + test (pull_request) Successful in 26s
create-release / create-release (pull_request) Has been skipped
dd257208cb
`UsageLimiter::acquire` atomically bumps the in-process concurrency map AND the durable daily counter, with no way to take a concurrency slot without counting toward the daily cap. bunyip BUNYIP-43 needs to meter only logical pulls (tag-addressed manifest requests) while still concurrency-bounding the digest-addressed follow-up manifest fetches. Because the only API coupled both, bunyip gated the whole acquire behind "is this a tag request", so digest-addressed fetches took no concurrency slot at all: a multi-arch `docker pull` issues one tag request then N by-digest platform-manifest fetches that went unbounded, defeating `concurrent_manifests_per_user` for the request type a multi-arch pull emits most of.

Add `acquire_concurrency_only(user_id) -> Result<UsageGuard, LimitDenial>`: takes a concurrency slot with no daily branch and no store call (so it is synchronous and cannot fail with AppError); the only denial is LimitDenial::Concurrency. The slot shares the same per-user concurrency budget as `acquire`, so a consumer can hold a slot for every request but count daily only for the ones representing a logical operation. The existing coupled `acquire` is unchanged (additive API); both now take the slot through a shared `try_take_concurrency_slot` helper, and `acquire`'s rollback paths release via guard drop (identical semantics: slot released before any store call).

Tests: concurrency-only does not increment the daily counter and never hits the daily cap (leaving the daily budget for a later coupled acquire); concurrency-only enforces the per-user concurrency cap and frees the slot on drop; and a concurrency-only slot shares the budget with a coupled acquire (which is then denied for concurrency without counting daily).

#PSA-42

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
nrupard deleted branch feat/psa-42-acquire-concurrency-only 2026-06-03 20:37:17 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
psa-systems/dunite!11
No description provided.