Skip to content

Quotas and admission control for multi-tenant daemons #13

Description

@rrrodzilla

Summary

Per-caller rate limits and token quotas, a bounded admission queue with priority classes, and backpressure instead of unbounded memory growth when requests outpace providers.

Motivation

Once a daemon serves more than one consumer (multiple internal teams, an HTTP frontend, several scheduled jobs), fairness and overload behavior become operational requirements. The actor mailbox model already provides most of the machinery. This feature makes it configurable and observable.

Current state as of 0.35.0

Several pieces this issue assumed would be built from scratch now exist, but all of them are keyed by provider, never by caller. Nothing in the codebase carries a caller or tenant identity.

Already built:

  • Pre-flight spend enforcement. accounting::budget asks the accountant before every provider dispatch and refuses with a typed ActonAIErrorKind::BudgetExceeded. Scopes are BudgetScope::{Total, Provider}. Money is integer micro-USD, unpriced usage fails closed.
  • Provider rate limits. RateLimitConfig carries requests_per_minute and tokens_per_minute per provider.
  • A bounded queue. The provider actor queues when rate limited and rejects past max_queue_size (0 means unlimited), so the unbounded-growth half of the motivation is handled per provider.
  • Operator pause and drain. introspection::admission::AdmissionState is a global Running / Paused / Draining switch checked once at the top of a turn. Useful, but it is an operator kill switch rather than admission control.
  • Observability (OpenTelemetry export: traces and metrics for the prompt loop #8) and the status surface (Live daemon introspection over IPC (acton-ai status / drain / resume) #10) both landed, so the reporting dependencies are unblocked.

Remaining scope

  • Caller identity on requests. An API key or tenant label carried through the existing correlation-ID plumbing. Nothing exists today; this is the piece everything else hangs off.
  • Caller-scoped aggregation. Extend the accountant's aggregation keys and add a BudgetScope::Caller, so requests-per-window and tokens-per-window can be enforced per caller with the machinery already there.
  • Priority classes on the admission queue. The existing queue is FIFO and per provider. Classes belong to one owning actor at the front of the request path, not to each provider.
  • A typed rejection that distinguishes over-quota from overloaded. Budget denial is already typed; queue-full is not distinguished from it, and a caller cannot currently tell "you are over your quota" from "the system is saturated, retry".
  • Queue depth and rejection counters as metrics. Queue size appears today only as a tracing field on the provider actor, not as an exported counter or gauge.

Architecture notes

  • Admission decisions belong to one owning actor at the front of the request path, not checks scattered across providers. The provider-level queue stays where it is as a last line of defense.
  • Rejection is a normal, recorded outcome with a typed error (#[non_exhaustive] enum per existing error conventions), never a silent drop.
  • Defaults must be off/unlimited so single-user library consumers see zero behavior change, matching how budgets, audit, and checkpoints all default.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions