Repository¶
The Repository class owns all DynamoDB data access and infrastructure management.
RateLimiter delegates data operations to the repository while owning business logic.
Preferred Usage Pattern¶
from zae_limiter import RateLimiter, Repository
# Open repository (auto-provisions if needed, recommended)
repo = await Repository.open()
limiter = RateLimiter(repository=repo)
# For explicit infrastructure provisioning, use builder:
repo = await Repository.builder().build()
limiter = RateLimiter(repository=repo)
# Infrastructure managed elsewhere (your own CloudFormation/Terraform/CDK):
repo = await Repository.connect("my-app")
limiter = RateLimiter(repository=repo)
See ADR-108 for the design rationale.
Choosing an Entry Point¶
| Entry point | Provisions infrastructure? | Use when |
|---|---|---|
Repository.open() |
Yes — deploys the stack, registers namespaces, updates the Lambda | Application code, prototyping, LocalStack dev |
Repository.builder().build() |
Yes — with custom options (permission boundaries, IAM naming, Lambda config) | Enterprise deployments |
Repository.connect() |
No — reads only, raises when anything is missing | Infrastructure owned by your own CloudFormation, Terraform, or CDK, or an application role that may only acquire |
An acquire-only role cannot set up a stack (v0.17.0)
Since v0.17.0 the acquire policies (AcquireOnlyPolicy, NamespaceAcquirePolicy)
allow writes to rate-limit buckets only (ADR-152). open() writes more than that
when there is something to set up: the first time it meets a stack or namespace
(registering the namespace, writing the version record), and whenever the stack's
Lambdas are older than the client (auto_update). Under an acquire-only role those
writes fail with an access error.
- Set up and upgrade with an operator role holding
FullAccessPolicy/NamespaceFullAccessPolicy:zae-limiter deploy,zae-limiter upgrade,zae-limiter namespace register, or oneopen()run under that role. - Have acquire-only applications call
connect(), which only reads. Once setup is done,open()also only reads, but it would try to update the Lambdas again after the next client upgrade.
Repository.connect()¶
connect() binds to infrastructure that already exists and never writes to it.
It issues two reads — a namespace lookup and a version record read — and raises
rather than repairing anything it finds missing:
| Situation | open() |
connect() |
|---|---|---|
| Table missing | Deploys the stack | InfrastructureNotFoundError |
| Namespace unregistered | Registers it | NamespaceNotFoundError |
| Version record missing | Writes it | InfrastructureNotFoundError |
| Lambda version behind client | Updates the Lambda | VersionMismatchError |
from zae_limiter import RateLimiter, Repository
# Stack, table, and namespaces are all deployed by your own template
repo = await Repository.connect("my-app")
limiter = RateLimiter(repository=repo)
Register namespaces ahead of time with zae-limiter namespace register, or
from a template, so connect() can resolve them. A SyncRepository.connect()
counterpart is available with the same signature.
Repository¶
Repository
¶
Repository(name, region=None, endpoint_url=None, stack_options=None, config_cache_ttl=60, *, rejection_cache_ttl=DEFAULT_REJECTION_CACHE_TTL, rejection_cache_size=DEFAULT_REJECTION_CACHE_SIZE, stamp_ttl=DEFAULT_STAMP_TTL_SECONDS, _skip_deprecation_warning=False)
Async DynamoDB repository for rate limiter data.
Handles all DynamoDB operations including entities, buckets, limit configs, and transactions.
Use :meth:open to open a repository (recommended), or :meth:builder
for custom infrastructure options (permission boundaries, IAM config).
.. deprecated::
Direct construction via Repository(...) is deprecated and will be
removed in v1.0.0. Use Repository.open(...) or
Repository.builder().build() instead.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Resource identifier (e.g., "my-app"). Used as the CloudFormation stack_name and DynamoDB table_name. |
required |
region
|
str | None
|
AWS region (e.g., "us-east-1"). |
None
|
endpoint_url
|
str | None
|
Custom endpoint URL (e.g., LocalStack). |
None
|
stack_options
|
StackOptions | None
|
Configuration for CloudFormation infrastructure. Pass StackOptions to enable declarative infrastructure management. |
None
|
config_cache_ttl
|
int
|
TTL in seconds for config cache (default: 60, 0 to disable). Controls caching of resolved limit configs in resolve_limits(). |
60
|
rejection_cache_ttl
|
float
|
Seconds a cached bucket state may be used to reject a request without a DynamoDB call (default: 1.0, 0 to disable). ADR-147. |
DEFAULT_REJECTION_CACHE_TTL
|
rejection_cache_size
|
int
|
Maximum cached bucket states (default: 10,000). |
DEFAULT_REJECTION_CACHE_SIZE
|
stamp_ttl
|
float
|
Seconds a |
DEFAULT_STAMP_TTL_SECONDS
|
Example::
# Most users (auto-provisions if needed)
repo = await Repository.open("my-app")
# Custom infrastructure options
repo = await Repository.builder().namespace("my-app").build()
builder
classmethod
¶
Create a RepositoryBuilder for fluent configuration.
For most use cases, prefer :meth:open instead. Use builder()
when you need custom infrastructure options (permission boundaries,
Lambda config, IAM role naming).
Stack defaults mirror :meth:open: ZAEL_STACK env var
or "zae-limiter", namespace "default".
Example
repo = await ( Repository.builder() .namespace("my-app") .lambda_memory(512) .build() )
open
async
classmethod
¶
open(namespace=None, *, stack=None, region=None, endpoint_url=None, config_cache_ttl=60, auto_update=True, rejection_cache_ttl=DEFAULT_REJECTION_CACHE_TTL, rejection_cache_size=DEFAULT_REJECTION_CACHE_SIZE, stamp_ttl=DEFAULT_STAMP_TTL_SECONDS)
Open a repository, auto-provisioning infrastructure if needed.
This is the recommended entry point for most applications.
Namespace is the primary parameter — stack name defaults to
"zae-limiter" and is rarely needed.
Auto-provision behavior:
- If the DynamoDB table doesn't exist, deploys a new stack with default options (aggregator enabled).
- If the table exists but the namespace isn't registered, registers it.
- The
"default"namespace is always registered. - Version check and Lambda auto-update run on every call.
For custom infrastructure options (permission boundaries, Lambda
config, IAM role naming), use Repository.builder() instead.
Stack resolution: stack arg → ZAEL_STACK env var
→ "zae-limiter".
Namespace resolution: namespace arg → ZAEL_NAMESPACE
env var → "default".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
str | None
|
Namespace to open. Defaults to |
None
|
stack
|
str | None
|
Stack name. Defaults to |
None
|
region
|
str | None
|
AWS region (e.g., |
None
|
endpoint_url
|
str | None
|
Custom endpoint URL (e.g., LocalStack). |
None
|
config_cache_ttl
|
int
|
Config cache TTL in seconds (default: 60, 0 to disable). |
60
|
auto_update
|
bool
|
Auto-update Lambda on version mismatch (default: True). |
True
|
rejection_cache_ttl
|
float
|
Seconds a cached bucket state may be used to reject a request without a DynamoDB call (default: 1.0, 0 to disable). See ADR-147. |
DEFAULT_REJECTION_CACHE_TTL
|
rejection_cache_size
|
int
|
Maximum cached bucket states (default: 10,000). |
DEFAULT_REJECTION_CACHE_SIZE
|
stamp_ttl
|
float
|
Seconds a |
DEFAULT_STAMP_TTL_SECONDS
|
Returns:
| Type | Description |
|---|---|
Repository
|
Fully initialized Repository ready for use. |
Raises:
| Type | Description |
|---|---|
IncompatibleSchemaError
|
If schema migration is required. |
VersionMismatchError
|
If auto_update is False and versions differ. |
Example::
# Most users
repo = await Repository.open("my-app")
# Multi-tenant
repo_alpha = await Repository.open("tenant-alpha")
# Explicit stack
repo = await Repository.open("my-app", stack="custom-stack")
# Simplest (stack="zae-limiter", namespace="default")
repo = await Repository.open()
connect
async
classmethod
¶
connect(namespace=None, *, stack=None, region=None, endpoint_url=None, config_cache_ttl=60, rejection_cache_ttl=DEFAULT_REJECTION_CACHE_TTL, rejection_cache_size=DEFAULT_REJECTION_CACHE_SIZE, stamp_ttl=DEFAULT_STAMP_TTL_SECONDS)
Connect to existing infrastructure without provisioning anything.
Use this when the CloudFormation stack, the DynamoDB table, and the
namespace registry are managed outside the library — a packaged
CloudFormation template, Terraform, or CDK. Unlike :meth:open,
connect() never creates or mutates infrastructure: it issues
reads only and raises when something it needs is absent.
Differences from :meth:open:
============================ =============== ==========================
Situation open() connect()
============================ =============== ==========================
Table missing Deploys stack InfrastructureNotFound
Namespace unregistered Registers it NamespaceNotFoundError
Version record missing Writes it InfrastructureNotFound
Lambda version behind client Updates Lambda VersionMismatchError
============================ =============== ==========================
Stack resolution: stack arg → ZAEL_STACK env var
→ "zae-limiter".
Namespace resolution: namespace arg → ZAEL_NAMESPACE
env var → "default".
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
str | None
|
Namespace to connect to. Defaults to
|
None
|
stack
|
str | None
|
Stack name. Defaults to |
None
|
region
|
str | None
|
AWS region (e.g., |
None
|
endpoint_url
|
str | None
|
Custom endpoint URL (e.g., LocalStack). |
None
|
config_cache_ttl
|
int
|
Config cache TTL in seconds (default: 60, 0 to disable). |
60
|
rejection_cache_ttl
|
float
|
Seconds a cached bucket state may be used to reject a request without a DynamoDB call (default: 1.0, 0 to disable). See ADR-147. |
DEFAULT_REJECTION_CACHE_TTL
|
rejection_cache_size
|
int
|
Maximum cached bucket states (default: 10,000). |
DEFAULT_REJECTION_CACHE_SIZE
|
stamp_ttl
|
float
|
Seconds a |
DEFAULT_STAMP_TTL_SECONDS
|
Returns:
| Type | Description |
|---|---|
Repository
|
Repository bound to the existing infrastructure. |
Raises:
| Type | Description |
|---|---|
InfrastructureNotFoundError
|
If the table doesn't exist, or
exists but was never initialized by |
NamespaceNotFoundError
|
If the namespace isn't registered. |
VersionMismatchError
|
If the deployed Lambda version is behind the client version. Redeploy your stack to resolve. |
IncompatibleSchemaError
|
If schema migration is required. |
Example::
# Infrastructure deployed by your own CloudFormation
repo = await Repository.connect("my-app")
limiter = RateLimiter(repository=repo)
namespace
async
¶
Return a scoped Repository for the given namespace.
The scoped repo shares the DynamoDB client, entity cache, and
namespace cache with the parent, but has its own ConfigCache
and namespace identity. Calling close() on a scoped repo
is a no-op (it does not close the shared client).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Namespace name to resolve. |
required |
on_unavailable
|
OnUnavailableAction | None
|
Override on_unavailable behavior for this
namespace ("allow" or "block"). Persisted via
|
None
|
bucket_ttl_multiplier
|
int | None
|
Override bucket TTL multiplier for this scoped repo. Defaults to the parent's value. |
None
|
Returns:
| Type | Description |
|---|---|
Repository
|
A new Repository scoped to the resolved namespace. |
Raises:
| Type | Description |
|---|---|
NamespaceNotFoundError
|
If the namespace is not registered. |
delete_stack
async
¶
Delete the CloudFormation stack and all associated resources.
Permanently removes the stack including DynamoDB table, Lambda aggregator, IAM roles, and CloudWatch log groups. Waits for deletion to complete. No-op if stack doesn't exist.
Raises:
| Type | Description |
|---|---|
StackOperationError
|
If deletion fails. |
ensure_infrastructure
async
¶
Ensure DynamoDB infrastructure exists.
.. deprecated::
Use Repository.builder(...).build() instead, which handles
infrastructure creation during the build step.
Creates CloudFormation stack using stack_options passed to the constructor. No-op if stack_options was not provided.
Raises:
| Type | Description |
|---|---|
StackOperationError
|
If CloudFormation stack creation fails |
create_stack
async
¶
Create DynamoDB infrastructure via CloudFormation.
.. deprecated:: 0.6.0
Use :meth:ensure_infrastructure instead. Pass stack_options
to the Repository constructor. Will be removed in v1.0.0.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stack_options
|
StackOptions | None
|
Configuration for CloudFormation stack. If None, uses the stack_options passed to the constructor. |
None
|
Raises:
| Type | Description |
|---|---|
StackOperationError
|
If CloudFormation stack creation fails |
register_namespace
async
¶
Register a namespace in the registry (idempotent).
Creates forward (name -> ID) and reverse (ID -> name) mappings. Idempotent: returns existing ID if namespace already registered.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
str
|
Namespace name to register. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The namespace_id (either newly created or existing). |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If namespace is the reserved namespace |
register_namespaces
async
¶
Bulk-register multiple namespaces.
Registers all namespaces in DynamoDB (forward + reverse records each).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespaces
|
list[str]
|
List of namespace names to register. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str]
|
Mapping of |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If any namespace is the reserved namespace |
get_namespace
async
¶
Get details for a single namespace by name.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
str
|
Namespace name to look up. |
required |
Returns:
| Type | Description |
|---|---|
dict[str, str] | None
|
Dict with |
dict[str, str] | None
|
or |
list_namespaces
async
¶
List all active namespaces with their IDs.
Performs a Query on PK = "_/SYSTEM#" with
SK begins_with "#NAMESPACE#" (forward records only).
Returns:
| Type | Description |
|---|---|
list[dict[str, str]]
|
List of |
delete_namespace
async
¶
Soft-delete a namespace. O(1) for data plane.
Removes the forward record and marks the reverse record as
status="deleted". Data items are NOT deleted — they remain
orphaned under the namespace's random ID prefix.
No-op if the namespace does not exist.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace
|
str
|
Namespace name to delete. |
required |
Raises:
| Type | Description |
|---|---|
ValidationError
|
If namespace is the reserved namespace |
recover_namespace
async
¶
Recover a deleted namespace by its ID.
Reads the reverse record to find the original name, re-creates the forward record, and marks the reverse record as active.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace_id
|
str
|
Opaque namespace ID to recover. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The recovered namespace name. |
Raises:
| Type | Description |
|---|---|
EntityNotFoundError
|
If the reverse record does not exist. |
ValidationError
|
If the namespace name is reserved. |
NamespaceStateError
|
If the namespace is active or being purged. |
list_orphan_namespaces
async
¶
List deleted namespaces with orphaned data.
Queries reverse records (SK begins_with "#NSID#") and
filters for status="deleted".
Returns:
| Type | Description |
|---|---|
list[dict[str, str]]
|
List of |
purge_namespace
async
¶
Purge all orphaned data for a deleted namespace.
Verifies the namespace is in "deleted" status, transitions to
"purging", queries GSI4 to find all items, deletes them in
batches, then removes the reverse record.
Safe to call on a non-existent namespace_id (no-op).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
namespace_id
|
str
|
Opaque namespace ID to purge. |
required |
Raises:
| Type | Description |
|---|---|
NamespaceStateError
|
If the namespace is |
RateLimiterUnavailable
|
DynamoDB kept some deletes unprocessed
after the retries. The namespace stays |
require_reset_after_readers
async
¶
Refuse a reset_after limit passed to acquire(limits=...) (#638 A).
An override is never stored as config, but the slow path writes it onto
the bucket item (b_{name}_rsa beside a zero refill rate) — exactly
the item a pre-v0.15 aggregator's shard clone mints a fresh share on. So
it is gated like a config write, but on the hot path, so at no cost
when it passes: the lambda_version that open(), connect()
or builder() already read is trusted. Only a refusal re-reads, with
one strongly consistent GetItem, so a stack upgraded since the
repository was opened is not refused on stale knowledge.
A repository that never read the record (the deprecated constructor) reads it once here, then trusts the cache like everyone else — one read per repository lifetime, against failing every such caller closed with no remedy on the same object. The ratchet does not run: nothing is stored as config, so no older client is made to misread a level.
Raises:
| Type | Description |
|---|---|
VersionMismatchError
|
as for the config writers. |
create_entity
async
¶
Create a new entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Unique identifier for the entity |
required |
name
|
str | None
|
Optional display name (defaults to entity_id) |
None
|
parent_id
|
str | None
|
Optional parent entity ID (for hierarchical limits) |
None
|
cascade
|
bool
|
If True, acquire() will also consume from parent entity |
False
|
metadata
|
dict[str, str] | None
|
Optional key-value metadata |
None
|
principal
|
str | None
|
Caller identity for audit logging |
None
|
Returns:
| Type | Description |
|---|---|
Entity
|
The created Entity |
Raises:
| Type | Description |
|---|---|
InvalidIdentifierError
|
If entity_id or parent_id is invalid |
EntityExistsError
|
If entity already exists |
get_entity
async
¶
Get an entity by ID.
consistent_read asks for a strongly consistent read (1 RCU instead
of 0.5): for a caller that must see a write it just made.
delete_entity
async
¶
Delete an entity and all its related records.
Deletes the entity's reset and top-up operation records, then every
bucket item GSI3 indexes under it, then every item under the entity's
PK (configs, usage snapshots, and #META last). The audit event
records how many items this call deleted (records_deleted) and
whether the entity is now gone (complete).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
ID of the entity to delete |
required |
principal
|
str | None
|
Caller identity for audit logging |
None
|
Raises:
| Type | Description |
|---|---|
RateLimiterUnavailable
|
DynamoDB kept some deletes unprocessed
after the retries (#742). What this call deleted is audited
with |
set_parent
async
¶
Move an entity to a new parent, or to no parent (ADR-150).
Rewrites the entity's META record and its GSI1 parent -> children keys,
bumping its parent generation, then restamps parent_id and the
effective cascade policy on every bucket the entity owns through
the ADR-146 cascade fan-out. Whether the entity debits its new parent
is the same decision as before the move: the per-resource cascade
policy (ADR-146), falling back to the entity's own cascade flag,
which a move does not change. An entity with no parent never cascades.
Nothing already debited moves: consumption charged to the old parent, including by leases open across the move, stays there.
Idempotent: repeating a move re-runs the fan-out, which is also how a
move that raised FanoutIncomplete is finished.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to move. |
required |
parent_id
|
str | None
|
The new parent, which must exist, or None for no parent. |
required |
principal
|
str | None
|
Caller identity for audit logging. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items stamped. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
The entity would become its own ancestor, or the new parent's chain is deeper than 32 levels. |
EntityNotFoundError
|
The entity or the new parent does not exist. |
VersionMismatchError
|
The stack's Lambdas predate parent moves. |
FanoutIncomplete
|
The META record moved but a bucket write failed part-way; re-run the same call to finish. |
get_bucket
async
¶
Get a single limit's bucket from the composite item.
get_buckets
async
¶
Get all buckets for an entity, optionally filtered by resource.
With pre-shard buckets (v0.9.0+), each item lives on its own partition
key PK={ns}/BUCKET#{id}#{resource}#{shard}. When resource is
specified, fetches the single bucket at the given shard_id. When
resource is None, uses GSI3 (KEYS_ONLY) to discover all bucket PKs,
then BatchGetItem to fetch full items.
The internal wcu infrastructure limit is filtered from the
returned bucket states.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to query buckets for. |
required |
resource
|
str | None
|
Resource name filter, or None for all resources. |
None
|
shard_id
|
int
|
Shard index (used only when resource is specified). |
0
|
Returns:
| Type | Description |
|---|---|
list[BucketState]
|
List of BucketState objects (one per application limit). |
batch_get_buckets
async
¶
Batch get composite buckets in a single DynamoDB call.
With composite items, each (entity_id, resource, shard) is a single DynamoDB item containing all limits. Returns individual BucketStates keyed by (entity_id, resource, limit_name) for backward compatibility.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[tuple[str, str, int]]
|
List of (entity_id, resource, shard_id) tuples. The shard is the one the acquire selected (issue #439); it is never assumed to be 0. |
required |
Returns:
| Type | Description |
|---|---|
dict[tuple[str, str, str], BucketState]
|
Dict mapping (entity_id, resource, limit_name) to BucketState. |
dict[tuple[str, str, str], BucketState]
|
Missing composite items are not included in the result. |
Note
DynamoDB BatchGetItem supports up to 100 items per request. For larger batches, this method automatically chunks the requests.
batch_get_entity_and_buckets
async
¶
Fetch entity metadata and composite buckets in a single BatchGetItem.
With composite items, each (entity_id, resource, shard) is a single DynamoDB item. Includes the entity's #META record alongside bucket records to avoid a separate get_entity() round trip.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity whose META record to include |
required |
bucket_keys
|
list[tuple[str, str, int]]
|
List of (entity_id, resource, shard_id) for composite buckets — the shard the acquire selected (issue #439) |
required |
Returns:
| Type | Description |
|---|---|
Entity | None
|
Tuple of (entity_or_none, bucket_dict) where bucket_dict maps |
dict[tuple[str, str, str], BucketState]
|
(entity_id, resource, limit_name) to BucketState. |
Note
DynamoDB BatchGetItem supports up to 100 items per request. The META key counts toward that limit.
batch_get_configs
async
¶
Batch get config items in a single DynamoDB call.
Fetches config records (entity, resource, system level) in a single BatchGetItem request and returns deserialized limits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[tuple[str, str]]
|
List of (PK, SK) tuples identifying config items |
required |
disabled_out
|
dict[tuple[str, str], bool | str | None] | None
|
Optional dict to receive the tri-state |
None
|
cascade_out
|
dict[tuple[str, str], bool | str | None] | None
|
The same, for the tri-state cascade policy (ADR-146), and for the same reason: a bucket created from a stale cached policy and then only ever hit on the fast path keeps it. |
None
|
Returns:
| Type | Description |
|---|---|
dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]
|
Dict mapping (PK, SK) to (limits, on_unavailable) tuples. |
dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]
|
on_unavailable is extracted from system config items (None for others). |
dict[tuple[str, str], tuple[list[Limit], OnUnavailableAction | None]]
|
Missing items are not included in the result. |
Note
DynamoDB BatchGetItem supports up to 100 items per request. For larger batches, this method automatically chunks the requests. Uses eventually consistent reads (0.5 RCU per item).
get_or_create_bucket
async
¶
Get an existing bucket or create a new one with the given limit.
If the bucket exists, it is returned. If not, a new bucket is created with capacity set to the limit's capacity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name (e.g., "gpt-4") |
required |
limit
|
Limit
|
Limit configuration for the bucket |
required |
Returns:
| Type | Description |
|---|---|
BucketState
|
Existing or newly created BucketState |
build_bucket_put_item
¶
Build a PutItem for a composite bucket (for use in transactions).
Wraps build_composite_create for backward compatibility with protocol.
build_bucket_update_item
¶
build_bucket_update_item(entity_id, resource, limit_name, new_tokens_milli, new_last_refill_ms, expected_tokens_milli=None, shard_id=0)
Build an UpdateItem for a single limit in a composite bucket.
Legacy method — prefer build_composite_normal/retry/adjust for composite writes. This updates one limit's tk within the composite item.
build_owner_check
¶
Build the ConditionCheck that pins a bucket create to its owner's META (ADR-150).
Rides in the create's own transaction: the owner's META must still
hold the parent generation and incarnation the create read (or still
be absent, when the read found none), so a move landing between the
read and the create cancels it instead of leaving the old parent on a
new bucket. Needs dynamodb:ConditionCheckItem. Writes nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
The owner whose bucket the transaction creates. |
required |
exists
|
bool
|
Whether the create's read found the owner's META. |
required |
pgen
|
int | None
|
The parent generation that read returned. |
required |
pinc
|
str | None
|
The incarnation that read returned. |
required |
build_composite_create
¶
build_composite_create(entity_id, resource, states, now_ms, ttl_seconds=86400, cascade=False, parent_id=None, shard_id=0, shard_count=1, vu=None, rf_ms=None, bypass_until_ms=None, pgen=None, pinc=None)
Build a PutItem for creating a new composite bucket.
Used on first acquire for an entity+resource. Condition ensures no concurrent creation race (attribute_not_exists).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name |
required |
states
|
list[BucketState]
|
BucketState objects for each limit |
required |
now_ms
|
int
|
Current timestamp in milliseconds |
required |
ttl_seconds
|
int | None
|
TTL in seconds from now, or None to omit TTL |
86400
|
cascade
|
bool
|
Whether the entity has cascade enabled |
False
|
parent_id
|
str | None
|
The entity's parent_id (if any) |
None
|
shard_id
|
int
|
Shard index for this bucket (default 0) |
0
|
shard_count
|
int
|
Total number of shards (default 1) |
1
|
vu
|
int | None
|
Valid-until stamp in epoch ms (#222 §2.1) — the earliest
instant at which any limit on this item changes effective
params. |
None
|
rf_ms
|
int | None
|
The |
None
|
bypass_until_ms
|
int | None
|
Stamp the item |
None
|
pgen
|
int | None
|
The owner's parent generation, read from its META with
|
None
|
pinc
|
str | None
|
The owner's incarnation, read with |
None
|
build_composite_normal
¶
build_composite_normal(entity_id, resource, consumed, refill_amounts, now_ms, expected_rf, ttl_seconds=None, shard_id=0, vu=None, clear_vu=False, windows=None, rf_ms=None, window_lengths=None, seeds=None, pin_shard_count=None, applied_windows=None, grant_counts=None, owner=None, pin_vu=False, expected_vu=None, soft_stamps=None, soft_trusted=frozenset(), owner_pgen=None, owner_pinc=None, read_tokens=None, cleared_top_ups=(), bypass_stamp=None, soft_reads=None)
Build an UpdateItem for the normal write path (ADR-115 path 2).
ADD tk:(refill - consumed), tc:consumed for each limit.
read_tokens (limit name -> tk as read, millitokens): each
limit whose refill delta is positive — a drip, a reset, a roll, a
window opened — is pinned on tk <= :read. The delta was computed
from that read, and a credit landing in between (a refund, a release)
moves neither rf nor vu: the refill would stack on top of it.
A pin that fails sends the write to the consumption-only retry, as a
lost rf lock does. A trim (delta <= 0) stays unpinned so it still
clamps.
SET rf:now. CONDITION rf = :expected.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name |
required |
consumed
|
dict[str, int]
|
Amount consumed per limit (millitokens) |
required |
refill_amounts
|
dict[str, int]
|
Refill amount per limit (millitokens) |
required |
now_ms
|
int
|
Current timestamp in milliseconds |
required |
expected_rf
|
int
|
Expected refill timestamp for optimistic lock |
required |
ttl_seconds
|
int | None
|
TTL behavior: - None: Don't change TTL - 0: REMOVE ttl (entity has custom limits) - >0: SET ttl to (now + ttl_seconds) |
None
|
shard_id
|
int
|
Shard index for this bucket (default 0) |
0
|
vu
|
int | None
|
Valid-until stamp in epoch ms (#222 §2.1), or |
None
|
clear_vu
|
bool
|
REMOVE |
False
|
pin_vu
|
bool
|
When this write sets or removes |
False
|
expected_vu
|
int | None
|
The |
None
|
windows
|
dict[str, tuple[int, int]] | None
|
Limit name -> The pair is one argument so that |
None
|
rf_ms
|
int | None
|
The |
None
|
window_lengths
|
dict[str, int] | None
|
Limit name -> |
None
|
seeds
|
dict[str, BucketState] | None
|
Limit name -> the state of a limit missing from this
existing item (#633): configured after the item was created,
so no Each seed is guarded by |
None
|
pin_shard_count
|
int | None
|
The shard count a quota seed's share, or a
reset or roll's grant (ADR-145 I4), was sized for, or |
None
|
applied_windows
|
dict[str, int] | None
|
Limit name -> the window start this write leaves
the limit's balance reflecting, stamped as |
None
|
soft_stamps
|
dict[str, int | None] | None
|
Limit name -> the deadline to stamp as
|
None
|
soft_reads
|
dict[str, int | None] | None
|
Limit name -> the deadline the pass read off the item
for that stamp (None when absent), for every name in
|
None
|
soft_trusted
|
frozenset[str]
|
Names the caller admitted as soft on authority it
trusts over the item's stamp (config read fresh, or an
|
frozenset()
|
owner_pgen
|
int | None
|
The owner's parent generation, read from its META with
|
None
|
owner_pinc
|
str | None
|
The owner's incarnation, read with |
None
|
cleared_top_ups
|
Sequence[str]
|
Quotas whose |
()
|
bypass_stamp
|
tuple[int | None, int | None] | None
|
|
None
|
build_composite_retry
¶
build_composite_retry(entity_id, resource, consumed, shard_id=0, seeds=None, soft_trusted=frozenset(), period_guards=None, now_ms=None)
Build an UpdateItem for the retry write path (ADR-115 path 3).
Lost optimistic lock — skip refill, only consume. ADD tk:(-consumed), tc:consumed for each limit. CONDITION: tk >= consumed per limit (prevent negative on acquire).
ReturnValuesOnConditionCheckFailure=ALL_OLD: a failure here is a
rejection, and the lease reports it from the item's real balance rather
than from the in-memory state that just proved stale (#633). Free — the
image rides on the failure response.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
seeds
|
dict[str, BucketState] | None
|
Limit name -> state of a limit missing from the item (#633),
exactly as passed to :meth: |
None
|
soft_trusted
|
frozenset[str]
|
As for :meth: |
frozenset()
|
now_ms
|
int | None
|
The instant a soft stamp's deadline is judged against (ADR-151 decision 8); None reads the clock. |
None
|
period_guards
|
dict[str, tuple[int | None, int | None]] | None
|
Limit name -> |
None
|
build_composite_adjust
¶
build_composite_adjust(entity_id, resource, deltas, shard_id=0, ceilings=None, trim=False, pin=None, *, tokens=True, counter=True)
Build an UpdateItem for the adjust write path (ADR-115 path 4).
An ADD for post-hoc correction. Can go negative by design.
Positive delta = consumed more (subtract tokens, add to counter).
Negative delta = consumed less (add tokens, subtract from counter).
A credit is self-trimming (#680, #721). With ceilings (limit name
-> CreditCeiling), every credited limit that has one is conditioned
on tk <= ceiling - credit: the credit lands only if the balance
stays at or below the ceiling. The ceiling is the caller's, computed
from a state that may be stale, so the condition also pins everything
that state's ceiling was computed from and that a write can lower: the
limit's stored cp, gc and schedule overrides, and, from
pin, the item's shard_count, vu and item-level schedule
strings (each None = absent). When the condition
fails — a reset edge, a refill to capacity, a doubling, a re-grant, a
changed limit — the caller re-issues the credit with trim=True: the
same ADD plus SET vu = 0 in one write. A debit, or a credit with
no known ceiling, carries no ceiling condition.
Every adjust is conditioned on attribute_exists(PK): an ADD on
an item that is gone (swept by TTL, deleted) would create a stub with no
parameters, which the speculative path then rejects forever. A failed
condition on a write with no other term therefore means the item is
gone, and the caller treats the adjustment as done.
Every refund, release, rollback and speculative compensation is built here, so the shard's cached state is forgotten here (ADR-147): tokens this process gives back must not be hidden by its own cache.
tokens=False writes tc only — a bypassed lease's adjustment,
release or rollback, which never debited tk (#311).
counter=False writes tk only — the refund of an enforce-shaped
write that landed on a bypassed bucket, whose consumption stays counted.
build_bypass_consume
¶
The slow path's write for a bypassed existing bucket (#311, ADR-151 §4).
ADD tc per limit and the renewed stamp, nothing else: no tk
debit, no refill, no rf advance, no reset or window roll, so lifting
the bypass needs no reset and leaves no debt. Conditioned on the item
carrying a bypass stamp, whatever its deadline: a bypass cleared
since this pass read config fails here, and the acquire is planned
again, enforced. Only a pass that read bypass config uncached builds
this write, so it is also the stamp's renewal (ADR-151 decision 8).
Positional tokens (#bc{i}).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
consumed
|
dict[str, int]
|
Limit name -> consumption, millitokens. |
required |
bypass_until_ms
|
int
|
The stamp's new deadline, at most T after this pass's uncached config read. |
required |
build_stamp_fix
¶
A rejected slow pass's stamp correction (ADR-151 decisions 4 and 8).
The stamps only: SET a deadline or REMOVE, for bypass and
each b_{name}_soft, as the config this pass read uncached says.
Nothing else on the item moves (no tk, tc, rf or vu),
so it admits nothing. Each stamp is pinned on its value as read, so a
fan-out or another pass that changed it since wins and this is a no-op.
Positional tokens (#xb, #xo{i}).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
bypass
|
tuple[int | None, int | None] | None
|
|
None
|
soft
|
dict[str, tuple[int | None, int | None]] | None
|
Limit name -> |
None
|
fix_bucket_stamps
async
¶
Write :meth:build_stamp_fix; False when a pin no longer matched.
Called by a slow pass about to raise RateLimitExceeded, which
otherwise writes nothing: an expired stamp config no longer backs then
kept every rejection on the slow path until refill (review E1). The
shard's cached state is forgotten once it lands (ADR-147), so the next
fast rejection is remembered from the corrected item. Any error other
than a lost pin propagates.
transact_write
async
¶
Execute a write, using single-item API when possible to halve WCU cost.
Returns the item's ALL_NEW attributes for a single-item update
(free on UpdateItem; a refund pins the image, #721), else None.
client_request_token makes a multi-item transaction idempotent for
ten minutes (TransactWriteItems' ClientRequestToken), so an SDK
retry of one whose response was lost succeeds instead of failing its
conditions. A single item ignores it.
write_each
async
¶
Write items independently without cross-item atomicity (1 WCU each).
Each item is dispatched as a single PutItem, UpdateItem, or DeleteItem call. Use for unconditional writes (e.g., ADD adjustments) where partial success is acceptable.
Returns one entry per item: the Attributes an UpdateItem returned
(non-empty only when the item asked for ReturnValues), else {}.
speculative_consume
async
¶
speculative_consume(entity_id, resource, consume, ttl_seconds=None, shard_id=None, now_ms=None, avoid_shards=frozenset(), avoid_parent_shards=frozenset())
Attempt speculative UpdateItem with condition check.
Checks entity cache for cascade metadata. If cache hit + cascade, issues child+parent UpdateItems concurrently via asyncio.gather and returns nested parent_result.
When shard_id is explicitly provided, targets that shard directly
without cascade logic (used for shard retry).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name |
required |
consume
|
dict[str, int]
|
Amount per limit (tokens, not milli) |
required |
ttl_seconds
|
int | None
|
TTL in seconds from now, or None for no TTL change |
None
|
shard_id
|
int | None
|
Explicit shard to target (skips random selection and cascade logic). None means auto-select from entity cache. |
None
|
now_ms
|
int | None
|
The caller's "now" (issue #430). The limiter passes the
same reading it used for its own refill math, so one logical
|
None
|
avoid_shards
|
frozenset[int]
|
Child shards the rejection cache knows are short (ADR-147); the child shard is drawn among the others. |
frozenset()
|
avoid_parent_shards
|
frozenset[int]
|
The same for the parent's shards on the parallel cascade write (ADR-147 phase 2, decision 4). |
frozenset()
|
Returns:
| Type | Description |
|---|---|
SpeculativeResult
|
SpeculativeResult with: |
SpeculativeResult
|
|
SpeculativeResult
|
|
SpeculativeResult
|
|
select_shard
¶
Pick the bucket shard an acquire should target (GHSA-76rv, issue #439).
This is the only place a shard is drawn. The speculative fast path
draws here, and the slow path reuses whatever shard the fast path
already selected (passing it back as shard_id) so a
BUCKET_MISSING on shard N reads and creates shard N — never a
second random draw that lands on shard 0 again.
Selection is random.randrange(shard_count) from the cached
shard_count, not a hash of the entity id: every call re-picks, which
is what spreads one hot entity's writes across all of its shards.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket. |
required |
resource
|
str
|
Resource name. |
required |
shard_id
|
int | None
|
Explicit shard to honour verbatim, or None to draw one. |
None
|
shard_count
|
int | None
|
Count observed by the caller (e.g. on a speculative failure image, which never updates the cache); None reads the entity cache. |
None
|
avoid
|
frozenset[int]
|
Shards the rejection cache knows cannot fit this request (ADR-147). The draw is uniform over the others; when every shard is in it the caller has already rejected, so it is ignored rather than leaving nothing to draw from. |
frozenset()
|
Returns:
| Type | Description |
|---|---|
int
|
|
int
|
the entity cache (1 when unknown). |
bump_shard_count
async
¶
Double shard_count on shard 0 via conditional write.
Shard 0 is the source of truth for shard_count. The conditional
expression shard_count = :old prevents double-bumping when
multiple clients race to double concurrently. Also updates the
entity cache with the new shard_count.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket. |
required |
resource
|
str
|
Resource name. |
required |
current_count
|
int
|
Current shard_count to double. |
required |
Returns:
| Type | Description |
|---|---|
int
|
The new shard_count (doubled), or — if another client already |
int
|
doubled (ConditionalCheckFailedException) — the winner's count |
int
|
read from the failed write's ALL_OLD image, so the loser draws |
int
|
from the new shard range instead of caching its stale count and |
int
|
landing back on the exhausted shard (issue #439). |
set_limits
async
¶
set_limits(entity_id, limits, resource=DEFAULT_RESOURCE, principal=None, *, disabled=PRESERVE_DISABLED, cascade=PRESERVE_CASCADE)
Store limit configs for an entity (composite format, ADR-114).
All limits for an entity+resource are stored in a single composite item with SK '#CONFIG#{resource}'. This reduces cache-miss cost from N GetItem calls to 1 GetItem call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
ID of the entity |
required |
limits
|
list[Limit]
|
List of Limit configurations to store |
required |
resource
|
str
|
Resource name (defaults to "default") |
DEFAULT_RESOURCE
|
principal
|
str | None
|
Caller identity for audit logging |
None
|
disabled
|
bool | str | None
|
Tri-state disabled flag. Defaults to preserving whatever
value is already stored (this is a full-replace PutItem, so an
explicit value must be passed to change it; see ADR-125).
Passing an explicit value also fans out to existing buckets,
exactly as |
PRESERVE_DISABLED
|
cascade
|
bool | None
|
Tri-state cascade policy (ADR-146), preserved the same way.
An explicit value is gated on the stack's version and fans out
like |
PRESERVE_CASCADE
|
Raises:
| Type | Description |
|---|---|
VersionMismatchError
|
|
reconcile_bucket_to_defaults
async
¶
Reconcile bucket to effective defaults after config deletion (issue #327).
Updates limit fields (cp, ra, rp) to match the new effective limits, sets TTL (since entity is now on defaults), and removes stale limit attributes that no longer exist in the effective config.
No-op if bucket doesn't exist (uses attribute_exists(PK) condition).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
ID of the entity |
required |
resource
|
str
|
Resource name |
required |
effective_limits
|
list[Limit]
|
The new effective limits (resource/system defaults) |
required |
stale_limit_names
|
set[str] | None
|
Limit names to REMOVE from bucket (limits that were in the deleted entity config but not in effective defaults) |
None
|
get_limits
async
¶
Get stored limit configs for an entity (composite format, ADR-114).
Reads a single composite item with SK '#CONFIG#{resource}' containing all limits for the entity+resource pair.
get_entity_disabled
async
¶
Read the disabled value from an entity config item.
Returns:
| Type | Description |
|---|---|
bool | str | None
|
True or False when explicitly set, |
bool | str | None
|
(#311), None when unset (inherit). |
get_entity_cascade
async
¶
Read the tri-state cascade policy from an entity config item (ADR-146).
Returns:
| Type | Description |
|---|---|
bool | None
|
True or False when explicitly set, None when unset (inherit). |
delete_limits
async
¶
Delete stored limit configs for an entity (composite format, ADR-114).
Deletes the single composite config item for this entity+resource.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
ID of the entity |
required |
resource
|
str
|
Resource name (defaults to "default") |
DEFAULT_RESOURCE
|
principal
|
str | None
|
Caller identity for audit logging |
None
|
list_entities_with_custom_limits
async
¶
List all entities that have custom limit configurations for a resource.
Uses GSI3 sparse index for efficient queries. Only entity-level configs have GSI3 attributes, so this query returns only entities with custom limits (not system or resource defaults).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource to filter by (required). |
required |
limit
|
int | None
|
Maximum number of entities to return. None for all. |
None
|
cursor
|
str | None
|
Pagination cursor from previous call. None for first page. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[list[str], str | None]
|
Tuple of (entity_ids, next_cursor). next_cursor is None if no more results. |
list_resources_with_entity_configs
async
¶
List all resources that have entity-level custom limit configs.
Uses the entity config resources registry (wide column with ref counts) for efficient O(1) lookup. Returns resources with count > 0.
Returns:
| Type | Description |
|---|---|
list[str]
|
Sorted list of resource names with at least one entity having custom limits |
set_resource_defaults
async
¶
set_resource_defaults(resource, limits, principal=None, *, disabled=PRESERVE_DISABLED, cascade=PRESERVE_CASCADE)
Store default limit configs for a resource (composite format, ADR-114).
All limits for a resource are stored in a single composite item with SK '#CONFIG'. This reduces cache-miss cost.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
limits
|
list[Limit]
|
List of Limit configurations to store |
required |
principal
|
str | None
|
Caller identity for audit logging |
None
|
disabled
|
bool | str | None
|
Tri-state disabled flag. Defaults to preserving whatever
value is already stored (this is a full-replace PutItem, so an
explicit value must be passed to change it; see ADR-125).
Passing an explicit value also fans out to existing buckets,
exactly as |
PRESERVE_DISABLED
|
cascade
|
bool | None
|
Tri-state cascade policy (ADR-146), preserved the same way.
An explicit value is gated on the stack's version and fans out
like |
PRESERVE_CASCADE
|
Raises:
| Type | Description |
|---|---|
VersionMismatchError
|
|
get_resource_defaults
async
¶
Get stored default limit configs for a resource (composite format, ADR-114).
Reads a single composite item with SK '#CONFIG' containing all limits for the resource.
get_resource_disabled
async
¶
Read the disabled value from a resource config item.
Returns:
| Type | Description |
|---|---|
bool | str | None
|
True or False when explicitly set, |
bool | str | None
|
(#311), None when unset (inherit). |
get_resource_cascade
async
¶
Read the tri-state cascade policy from a resource config item (ADR-146).
Returns:
| Type | Description |
|---|---|
bool | None
|
True or False when explicitly set, None when unset (inherit). |
delete_resource_defaults
async
¶
Delete stored default limit configs for a resource (composite format, ADR-114).
Deletes the single composite config item for this resource.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
principal
|
str | None
|
Caller identity for audit logging |
None
|
list_resources_with_defaults
async
¶
List all resources that have default limit configs from the resource registry.
set_system_defaults
async
¶
Store system-wide default limits and config (composite format, ADR-114).
All system limits and config (on_unavailable) are stored in a single composite item with SK '#CONFIG'. This reduces cache-miss cost.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limits
|
list[Limit]
|
List of Limit configurations (apply to all resources) |
required |
on_unavailable
|
OnUnavailableAction | None
|
Behavior when DynamoDB unavailable ("allow" or "block") |
None
|
principal
|
str | None
|
Caller identity for audit logging |
None
|
Raises:
| Type | Description |
|---|---|
VersionMismatchError
|
|
get_system_defaults
async
¶
Get system-wide default limits and config (composite format, ADR-114).
Reads a single composite item with SK '#CONFIG' containing all limits and on_unavailable setting.
Returns:
| Type | Description |
|---|---|
tuple[list[Limit], OnUnavailableAction | None]
|
Tuple of (limits, on_unavailable). on_unavailable may be None if not set. |
delete_system_defaults
async
¶
Delete all system-wide default limits and config (composite format, ADR-114).
Deletes the single composite config item for system defaults.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
principal
|
str | None
|
Caller identity for audit logging |
None
|
get_system_limits
async
¶
Get system-wide default limits (without config).
This is a convenience method that returns only the limits. Use get_system_defaults() to also get on_unavailable config.
get_provisioner_state
async
¶
Get the provisioner state record for this namespace.
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Dict with keys: managed_system, managed_resources, managed_entities, |
dict[str, Any]
|
last_applied, applied_hash. Returns empty state if no record exists. |
put_provisioner_state
async
¶
Write the provisioner state record for this namespace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
dict[str, Any]
|
Dict with keys: managed_system, managed_resources, managed_entities, last_applied, applied_hash. |
required |
get_version_record
async
¶
Get the infrastructure version record.
Returns:
| Type | Description |
|---|---|
dict[str, Any] | None
|
Version record with schema_version, lambda_version, etc. |
dict[str, Any] | None
|
None if no version record exists. |
ping
async
¶
Check if the DynamoDB table is reachable.
Performs a lightweight GetItem operation to verify connectivity. Does not verify the table is initialized or has valid data.
Returns:
| Type | Description |
|---|---|
bool
|
True if the table is reachable, False otherwise. |
set_version_record
async
¶
Set the infrastructure version record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
schema_version
|
str
|
Current schema version (e.g., "1.0.0") |
required |
lambda_version
|
str | None
|
Currently deployed Lambda version |
None
|
client_min_version
|
str | None
|
Minimum compatible client version. |
None
|
updated_by
|
str | None
|
Identifier of what performed the update |
None
|
get_audit_events
async
¶
Get audit events for an entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
ID of the entity to query |
required |
limit
|
int
|
Maximum number of events to return |
100
|
start_event_id
|
str | None
|
Event ID to start after (for pagination) |
None
|
Returns:
| Type | Description |
|---|---|
list[AuditEvent]
|
List of AuditEvent objects, ordered by most recent first |
get_usage_snapshots
async
¶
get_usage_snapshots(entity_id=None, resource=None, window_type=None, start_time=None, end_time=None, limit=100, next_key=None)
Query usage snapshots with filtering and pagination.
Supports two query modes: 1. Entity-scoped: Query by entity_id (uses primary key) 2. Resource-scoped: Query by resource across all entities (uses GSI2)
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str | None
|
Entity to query (mutually exclusive for efficient queries) |
None
|
resource
|
str | None
|
Resource name filter (required if entity_id is None) |
None
|
window_type
|
str | None
|
Filter by window type ("hourly", "daily") |
None
|
start_time
|
str | None
|
Filter snapshots >= this timestamp (ISO format) |
None
|
end_time
|
str | None
|
Filter snapshots <= this timestamp (ISO format) |
None
|
limit
|
int
|
Maximum items to fetch from DynamoDB per page (default: 100) |
100
|
next_key
|
dict[str, Any] | None
|
Pagination cursor from previous call |
None
|
Returns:
| Type | Description |
|---|---|
tuple[list[UsageSnapshot], dict[str, Any] | None]
|
Tuple of (snapshots, next_key). next_key is None if no more results. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither entity_id nor resource is provided |
Note
The limit parameter controls the DynamoDB query batch size.
Client-side filters (window_type, start_time, end_time) are applied
after fetching, so the returned count may be less than limit.
Use next_key to paginate through all matching results.
get_usage_summary
async
¶
Aggregate usage across snapshots into a summary.
Fetches all matching snapshots (auto-paginates internally) and computes: - Total consumption per limit type - Average consumption per snapshot per limit type - Time range of aggregated data
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str | None
|
Entity to query |
None
|
resource
|
str | None
|
Resource name filter |
None
|
window_type
|
str | None
|
Filter by window type ("hourly", "daily") |
None
|
start_time
|
str | None
|
Filter snapshots >= this timestamp (ISO format) |
None
|
end_time
|
str | None
|
Filter snapshots <= this timestamp (ISO format) |
None
|
Returns:
| Type | Description |
|---|---|
UsageSummary
|
UsageSummary with aggregated statistics |
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither entity_id nor resource is provided |
get_resource_buckets
async
¶
Get all buckets for a resource across all entities.
With composite items, each GSI2 entry is one composite item per entity. Returns individual BucketStates, optionally filtered by limit_name.
resolve_limits
async
¶
Resolve effective limits using the four-level config hierarchy.
Uses ConfigCache with batched fetch optimization. Falls back to sequential individual GetItem calls if batch resolution fails.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to resolve limits for |
required |
resource
|
str
|
Resource being accessed |
required |
disabled_out
|
dict[tuple[str, str], bool | str | None] | None
|
Optional dict to receive the tri-state |
None
|
cascade_out
|
dict[tuple[str, str], bool | str | None] | None
|
The same, for the tri-state cascade policy (ADR-146);
see |
None
|
Returns:
| Type | Description |
|---|---|
tuple[list[Limit] | None, OnUnavailableAction | None, ConfigSource | None]
|
Tuple of (limits, on_unavailable, config_source) |
resolve_on_unavailable
async
¶
Resolve on_unavailable from system config, with caching fallback.
Returns the on_unavailable action from system config. Caches the value after first successful load so it's available as fallback when DynamoDB is unreachable. Defaults to "block" if no system config exists and no cached value is available.
resolve_disabled_from_fetched
¶
Answer the disable walk from a config fetch, or decline (ADR-125).
resolve_limits(disabled_out=...) records the tri-state disabled of
every level it actually read. The disable walk's levels are a subset of
those, so when all of them were read in that same call, the walk can be
evaluated here for free instead of issuing an identical second
BatchGetItem.
Returns None — meaning "the caller must call resolve_disabled" — if
even one level is missing from fetched. A missing level is one the
config cache served, and a cached value must never answer this gate:
a first acquire() for an entity with no bucket yet would then be
admitted on a stale false, and would go on to create an unstamped
bucket that the already-finished fan-out will never stamp. That is
permanent admission to a disabled resource, which is why
resolve_disabled is uncached in the first place.
Note the distinction this relies on: a level present in fetched with
value None was genuinely read and has no explicit value (so the walk
moves on); a level absent from fetched was not read at all.
resolve_bypass_from_fetched
¶
Whether the walk resolves to bypass, from a config fetch, or decline (#311).
The resolve_disabled_from_fetched contract: None when any level of the
walk was served from the config cache, which must never answer it.
resolve_cascade_from_fetched
¶
Answer the cascade-policy walk from a config fetch, or decline (ADR-146).
The resolve_disabled_from_fetched contract, for resolve_limits(
cascade_out=...): None means "the caller must call resolve_access",
because a level the config cache served must not answer it — a bucket
created from a stale cached policy and then only ever hit on the fast
path would keep it.
Returns:
| Type | Description |
|---|---|
tuple[bool | None, str | None] | None
|
|
tuple[bool | None, str | None] | None
|
meaning the entity's own META |
limits_read_fresh
¶
Did one config fetch read every level that decided these limits? (#467)
fetched is the disabled_out of the same resolve_limits call:
it holds exactly the config keys that call read from DynamoDB rather
than from the config cache. The limits were read fresh when the level
they came from (source) and every level of higher precedence were
all read — a cached higher level could hide a newer definition. A
limit's soft-ness rides with the limit (ADR-151), so a fresh read is
the authority the slow path re-stamps b_{name}_soft from.
resolve_soft_limits
async
¶
Each resolved limit's soft-ness, read uncached (#467, ADR-151 §6.2).
A bucket the slow path creates or seeds is stamped soft from here when
the config cache served this pass's limits: a stale soft stamp is
unbounded over-admission (soft -> hard on a bucket that then only ever
takes the fast path), where a stale capacity is merely late. One
BatchGetItem of the four config levels (2 RCU), paid only on a
create or seed whose limits came from the cache.
Returns:
| Type | Description |
|---|---|
dict[str, bool]
|
Limit name -> soft, for the limits the four-level resolution picks |
dict[str, bool]
|
(the first level defining any). Empty when no level defines one. |
resolve_disabled
async
¶
Resolve the effective disabled state for an entity+resource (ADR-125).
Walks entity(resource) -> entity(default) -> resource and returns the
first level that sets disabled explicitly. This walk is independent of
the limits walk in resolve_limits(): a level that sets disabled but
defines no limits still decides the outcome, which is what lets an
entity-level disabled: false re-admit one entity to a disabled resource.
Deliberately uncached — see ADR-125. Called only on the slow path and by the eager fan-out, never on the speculative fast path.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to resolve for |
required |
resource
|
str
|
Resource being accessed |
required |
Returns:
| Type | Description |
|---|---|
bool
|
(effective_disabled, deciding_level) where deciding_level is |
str | None
|
"entity", "entity_default", "resource", or None if nothing set it. |
resolve_access
async
¶
resolve_access(entity_id, resource, *, consistent_read=False, include_limits=False, include_system=False)
Resolve disabled and the cascade policy from one uncached read.
Both walks cover the same three config items (ADR-125, ADR-146), so a
slow path that needs both — the parent of a cascade, or any level the
config cache served — pays for one BatchGetItem, as it did for
disabled alone. Never cached, for the reasons resolve_disabled gives.
consistent_read is for a caller resolving a level it just wrote:
the cascade fan-out (pre-release review: an eventually consistent read
returned the old policy and the fan-out stamped every bucket with it).
include_limits also returns each resolved limit's soft-ness in
limit_soft (#467), resolved from the walk's own items: the slow
path stamps a created or seeded bucket from it when the config cache
served that pass's limits. include_system adds the system config
item to the same batch (+0.5 RCU), for limits the cache resolved from
the system level; without it a walk that defines no limit reads as
all-hard, the safe direction.
plan_quota_shard
async
¶
Plan the grant of every quota in limits for shard shard_id (ADR-145).
Reads the siblings once — one GSI3 KEYS_ONLY query + one
BatchGetItem, the #587 reclaim's cost — and decides per quota, with
the pure models.plan_quota_grant: a move off the current-period
sibling covering this slot, or a fresh grant when none covers it.
The count is the largest of shard_count and every sibling's stored
count, and each quota's share is its schedule-effective capacity at
now_ms over that count. Before a fresh grant, a sibling still below
that count has it raised (design §8 R5) — a write only when a lag is
seen — and a legacy (no gc) quota on it has its grant size frozen
at the count read, which is also the grant_count every planned
debit expects, so the move and the freeze agree. Writes nothing else:
the returned debits ride in the acquire's own transaction (I5), built
by :meth:build_quota_donor_debits.
Returns:
| Type | Description |
|---|---|
int
|
|
dict[str, QuotaGrant]
|
are empty when |
list[QuotaDonorDebit]
|
(no sibling to read); a quota absent from the grants takes its full |
tuple[int, dict[str, QuotaGrant], list[QuotaDonorDebit]]
|
share. |
repair_created_quota_shard
async
¶
Raise a quota shard this client just created, if a doubling overtook it (ADR-145).
A create Put is guarded only by attribute_not_exists(PK), not by
the count it planned at. Another client can double shard 0 between this
client's plan and its Put; that doubling's propagation finds no item
at this shard and writes nothing, so the Put lands at the old count.
The shard would then reset at the old count every period and cover the
slots a shard created at the new count also covers — a recurring
over-admission of one new share per period.
So after the Put lands: one strongly consistent projected
GetItem of shard 0's shard_count (1 RCU), and, only when it is
higher, the grant-size freeze (:meth:_build_quota_count_freeze):
SET shard_count = :new, gc = if_not_exists(gc, :old) under
shard_count < :new. The current grant keeps the size it was planned
at; the next reset grants at the new count. Shard 0 is never written.
Returns:
| Type | Description |
|---|---|
bool
|
True when the created item was raised. |
build_quota_donor_debits
¶
The donor side of each ADR-145 move, one Update per donor shard.
ADD tk -x under: the donor still exists; still holds x; its
grant count is the one read (or absent and equal to its shard count —
a v0.14 item, design §9); and its grant is still the current period
(calendar rf >= edge; session wa unchanged). Several quotas
moving off one donor share one Update: a transaction may touch an
item once. Tokens are positional (#634).
get_shard_window_starts
async
¶
Read one shard's duration-window starts, to seed a shard being created (ADR-140).
Shard 0 by default, because :meth:bump_shard_count already treats it
as the source of truth for shard_count. A created shard inherits
ws verbatim and stamps it as applied (wa = ws, #640), so
ws > wa is false on the new item and it does not immediately
re-roll itself: it joins the
window in progress rather than opening one. Whether the window read
here is still live is the caller's decision, against the rsa of
the config it resolved — this returns the stored start and nothing
more.
A separate read rather than an extra key in the create path's
BatchGetItem: that call returns a dict keyed by (entity_id,
resource, limit_name) with no shard component, so shard 0 and shard N
would collide on every key. Strongly consistent, so 1 RCU:
DynamoDB charges a GetItem on the full item size, not on the
projection, and bucket items stay inside the ~1 KB budget (#222 §4.2),
well under the 4 KB a single strongly consistent RCU covers. The
projection only trims the response. Once per shard ever (≤ 31 per
(entity, resource), plus TTL recreations) on a path already priced at
2.5 RCU + 2 WCU.
Consistency is load-bearing, not a nicety. A shard is usually created
right after a wcu doubling, which is usually right after shard 0
was written — including the write that rolled its window. An eventually
consistent read can return the pre-roll ws, which looks ended, so
the caller opens a fresh window at full share instead of taking the
ADR-145 move from the window shard 0 just opened: measured at 15
admitted against a quota of 10. The extra 0.5 RCU per shard creation
is the whole price of closing that.
A limit absent from the result has no window on that shard — either it carries none, or the shard has been swept. The caller then opens a fresh window, which is the degraded case ADR-139 records under Consequences and which idle-restarting makes correct rather than merely tolerable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket. On a cascade create this is the entity whose shard is being created — the parent for a parent shard, never the child. Parent and child windows are independent (ADR-139). |
required |
resource
|
str
|
Resource name. |
required |
limit_names
|
list[str]
|
The limits to look for; only these attributes are projected, so the read stays a fraction of the item. |
required |
shard_id
|
int
|
The shard to read. Defaults to 0. |
0
|
Returns:
| Type | Description |
|---|---|
dict[str, int]
|
|
dict[str, int]
|
the item. |
Raises:
| Type | Description |
|---|---|
RateLimiterUnavailable
|
A stored |
restamp_owner_if_current
async
¶
Claim a bucket stamped by another incarnation, if this one is current (ADR-150).
The slow path's owner pin refuses another incarnation's stamp, because
its META read is eventually consistent and may be a deleted
predecessor's. When that pin is lost to another incarnation, one
strongly consistent read of META (1 RCU) settles which is current: if
it is incarnation, the bucket is a predecessor's leftover and is
restamped through the fan-out's write, which lets the current
incarnation overwrite any other; if not, the caller read a stale
record and the bucket is left alone.
Returns:
| Type | Description |
|---|---|
bool
|
Whether the bucket was restamped. |
disable_resource
async
¶
Disable a resource for all entities without an explicit override (ADR-125).
Writes config first, then eagerly stamps every existing bucket so the change takes effect on the speculative fast path immediately.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource to disable |
required |
principal
|
str | None
|
Caller identity for audit logging |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items stamped. |
enable_resource
async
¶
Explicitly enable a resource (stores disabled: false).
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items unstamped. |
clear_resource_disabled
async
¶
Remove the resource's explicit disabled value, reverting to inherit.
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items unstamped. |
bypass_resource
async
¶
Bypass a resource for every entity without an explicit override (#311).
Every request is admitted and no limit's balance is debited, while
consumption is still counted (tc, usage snapshots); only the
reserved wcu limit still gates. Stored as disabled: "bypass",
the fourth value of the ADR-125 walk, so an entity-level
disabled: true still blocks that entity and an entity-level
false still enforces it. Writes config, then stamps every existing
bucket (bypass). clear_resource_disabled() lifts it.
Raises:
| Type | Description |
|---|---|
VersionMismatchError
|
the stack's Lambdas predate bypass (0.17.0). |
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items stamped. |
set_resource_cascade
async
¶
Set whether entities cascade to their parent on this resource (ADR-146).
An entity-level policy for the resource, or the entity's own
_default_ policy, still outranks it. Writes config, then restamps
every existing bucket of the resource (_fanout_cascade).
Raises:
| Type | Description |
|---|---|
VersionMismatchError
|
the stack's Lambdas predate the cascade policy. |
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items stamped. |
clear_resource_cascade
async
¶
Remove the resource's cascade policy, reverting to inherit (ADR-146).
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items restamped. |
disable_entity
async
¶
Disable an entity, for one resource or across all of them (ADR-125).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to disable |
required |
resource
|
str | None
|
Resource to scope to. None targets the entity's
|
None
|
principal
|
str | None
|
Caller identity for audit logging |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items written. When scoped to one resource, |
int
|
every written bucket is stamped |
int
|
|
int
|
buckets to its OWN resolved value (which may disagree with this |
int
|
call for a resource with its own override) — see |
int
|
|
enable_entity
async
¶
Explicitly enable an entity, overriding a disabled resource.
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items written. When scoped to one resource, |
int
|
every written bucket is unstamped ( |
int
|
attribute). When unscoped, |
int
|
(re)writes each of the entity's buckets to its OWN resolved |
int
|
value, which may disagree with this call — see |
bypass_entity
async
¶
Bypass an entity, for one resource or across all of them (#311).
Every request the entity makes is admitted and no limit's balance is
debited, while consumption is still counted; only the reserved wcu
limit still gates. An entity-level value outranks the resource's, so
this re-admits one entity to a disabled resource as well. A bypass
never extends to the entity's cascade parent, which resolves its own
mode (ADR-151 §8). clear_entity_disabled() lifts it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to bypass |
required |
resource
|
str | None
|
Resource to scope to. None targets the entity's
|
None
|
principal
|
str | None
|
Caller identity for audit logging |
None
|
Raises:
| Type | Description |
|---|---|
VersionMismatchError
|
the stack's Lambdas predate bypass (0.17.0). |
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items written, as for |
clear_entity_disabled
async
¶
Remove the entity's explicit disabled value, reverting to inherit.
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items restamped to match the inherited value. |
set_entity_cascade
async
¶
Set whether an entity cascades to its parent (ADR-146).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to configure. |
required |
cascade
|
bool
|
True to debit the parent on this entity's acquires. |
required |
resource
|
str | None
|
Resource to scope to. None targets the entity's
|
None
|
principal
|
str | None
|
Caller identity for audit logging. |
None
|
Raises:
| Type | Description |
|---|---|
VersionMismatchError
|
the stack's Lambdas predate the cascade policy. |
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items stamped, each with the policy resolved for |
int
|
its own resource. |
clear_entity_cascade
async
¶
Remove the entity's cascade policy, reverting to inherit (ADR-146).
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items restamped. |
reset_bucket
async
¶
Restore an entity's balance on one resource to its full share, now (ADR-149).
Every shard of the (entity, resource) bucket gets one rf-locked, zero-consumption pass in one transaction: every limit on it is materialised as an acquire would (refill, pending reset edge, pending window roll), then each named limit is restored to its full share:
- a dripping limit to its ceiling (debt is forgiven);
- a calendar quota to its share, starting a new period;
- a session quota's current window is marked ended, so the next admitted request opens a fresh one at its full share (the balance itself is left for that opener to restore).
Nothing else changes: the consumption counter, disabled,
shard_count, config, the parent's buckets and other resources are
left alone, and every shard's next acquire takes one materialising
pass. A bucket that does not exist yet is a no-op (the first acquire
creates it at full share). Not gated by version.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity whose balance to restore. |
required |
resource
|
str
|
Resource to restore. One resource at a time. |
required |
limits
|
Sequence[str] | None
|
Limit names to restore. |
None
|
principal
|
str | None
|
Caller identity for audit logging. |
None
|
idempotency_key
|
str | None
|
Names this operation (1-64 characters: letters,
digits, |
None
|
Returns:
| Type | Description |
|---|---|
BucketOperationResult
|
The shards written and, per limit, the tokens restored. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
A name is not a limit configured for the pair, or no limit is configured at all. |
RateLimiterUnavailable
|
Another writer kept changing the bucket through every retry. |
top_up
async
¶
Add tokens to an entity's balance on one resource, now (ADR-149).
amounts mirrors acquire(consume=...): limit name to whole
tokens. Every shard of the bucket gets one rf-locked, zero-consumption
pass in one transaction; a pending reset edge or window roll is
applied first, in the same write, so the credit lands in the current
period.
- A quota gains exactly the amount, spread over its shards by
the share of the allowance each one holds (
1 / gc). Every token a shard gains is recorded as allowance topped up this period (b_{name}_tu+= its portion), whatever the shard held, and lasts until the next reset or window roll. That needs Lambdas at 0.17.0 or later (an older aggregator clamps it away): the version is checked once per repository andclient_min_versionis raised to 0.17.0. - A session quota with no live window opens one now, so the purchase starts the session.
- A dripping limit gains at most the room below its ceiling (a refund, not a purchase); the amount granted is returned.
A plan upgrade is set_limits(...) with the new plan followed by
top_up() of the difference. A bucket that does not exist yet is
created on shard 0 holding its full share plus the quota top-up.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to credit. |
required |
resource
|
str
|
Resource to credit. |
required |
amounts
|
dict[str, int]
|
Whole tokens to add per limit name (each > 0). |
required |
principal
|
str | None
|
Caller identity for audit logging. |
None
|
idempotency_key
|
str | None
|
Names this operation (1-64 characters: letters,
digits, |
None
|
Returns:
| Type | Description |
|---|---|
BucketOperationResult
|
The shards written and, per limit, the tokens granted. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
An amount is not a positive integer, or a name is not a limit configured for the pair. |
VersionMismatchError
|
A quota top-up against Lambdas predating 0.17.0. |
ResourceDisabled
|
The bucket does not exist yet and the resource is disabled for the entity. |
RateLimiterUnavailable
|
Another writer kept changing the bucket through every retry. |
build_operation_record
¶
Build the Put of a reset or top-up's operation record (ADR-149).
One item per call, in the shard-0 bucket's partition, conditioned on
attribute_not_exists(PK) and written in the same transaction as the
shard writes: only one transaction carrying operation_id can ever
commit. It stores the result, which a retry that finds it returns, and
writer, the writing call's nonce: a call audits only an operation
its own transaction committed, never one a same-key duplicate did. It
stores the entity's incarnation too, and may replace a record of
an earlier incarnation (ent differs), which reads as absent.
"-" (no META) matches every incarnation on both sides: a record
written with it is never replaced, and a call made with it never
replaces one.
build_bucket_operation
¶
Build one shard's rf-locked reset or top-up Update (ADR-149 §4.2).
ADD a token delta per limit (never SET: a fast-path debit landing
between the read and the write is kept), SET rf monotonic, vu = 0
to force one materialising pass that clamps anything credited in
between, plus the grant counts, windows, applied markers and top-ups the
plan sets and the tu / wtc it removes. Conditioned on the
rf and shard_count read; tc, disabled and the owner
stamps are never touched, and ttl only when one is given. The
call's operation record rides in the same transaction (:meth:build_operation_record).
Tokens are positional (#634): #ot/:ot deltas, #og/:og
grant counts, #ow/:ow + #or/:or windows, #oa/:oa
applied markers, #ou/:ou top-ups, #ox / #oy removals,
#on/:on the calendar quota reset counters (b_{name}_rn),
#ottl/:ottl the ttl (epoch seconds) when one is given: a
bucket on resource or system defaults (:meth:_operation_ttl), or
REMOVE #ottl with remove_ttl: an entity-level bucket (ADR-136).
Never both: the caller passes ttl only for a defaults bucket.
invalidate_config_cache
async
¶
Invalidate all cached config entries (ADR-122).
RepositoryProtocol¶
The RepositoryProtocol defines the interface for pluggable backends.
Implement this protocol to use a different storage backend (e.g., for testing).
Changed in v0.17.0: the adjust, normal and retry writes' contracts (#721)
A custom backend must accept ceilings, trim and pin on
build_composite_adjust from v0.17.0. The lease and the limiter pass them on
every refund — a rollback, a release(), a negative adjust() or a
speculative compensation — so a backend that does not accept them raises
TypeError there. It must also accept read_tokens on
build_composite_normal and period_guards on build_composite_retry,
described below.
Required of every adjust write, whatever the keywords:
- The item must exist. Fail the write (raise
ConditionalCheckFailedException) when the bucket is gone. An unconditional credit onto a swept or deleted item creates a stub that rejects the entity forever. - A quota credit stays in its debit's period. For each credited limit
whose
CreditCeiling.quota_periodis set, fail the write unless the period still holds — calendar: the item'srfis beforereset_edge_msand the reset-schedule strings inreset_sched_raw(itemrsched,sched_tz, the limit'srsched) are still the item's; session: the limit's window start and applied marker equalwindow. An unconditional credit lands a refund of one period's debit in the next, which then admits more than its allowance. Applies to thetrim=Truewrite too.
Expected for the protection a refund relies on:
ceilings: limit name →CreditCeiling. Condition each credited limit on its balance staying at or belowceiling_milliafter the credit, and on the stored capacity, grant count and schedule overrides it was computed from. RaiseConditionalCheckFailedExceptionwhen the condition fails.trim=True: the fallback the caller sends after that failure. Apply the same credit (still subject to the two requirements above) and, in the same write, mark the bucket so the next acquire re-materialises it (SET vu = 0on DynamoDB).pin: the item's (shard_count,vu, item-level schedule strings) as the ceiling saw them, held beside the ceilings.
build_composite_normal(read_tokens=...): limit name → the tk the
slow pass read (millitokens). For every limit whose refill delta is
positive — a drip, a reset, a roll, a window opened — fail the write
unless the stored balance is still at or below that read. A refund or
release landing between the read and the write moves neither rf nor
vu, and an unpinned raise stacks on top of it. A trim (delta <= 0) is
not pinned. A failure falls to the consumption-only retry, like a lost
rf lock.
build_composite_retry(period_guards=...): limit name →
(reset_edge_ms, window_start_ms), one of the two set, for each quota the
lost write was to reset, roll or open. Fail the retry unless that period
has been applied on the item — calendar: rf >= reset_edge_ms; session:
the limit's window-applied marker equals window_start_ms — and return the
old image on that failure (ALL_OLD). The lease then re-plans once from a
fresh read; a second loss is a rejection. Without the guard the retry
spends the previous period's balance and leaves the reset pending, so the
next pass grants a full allowance again.
transact_write now returns the item's attributes after a single-item
update (ALL_NEW), or None. A refund after the consumption-only retry
takes its quota period from that image; a backend that returns None
makes it fall back to the period the lease read, which can drop a refund
after a reset raced the retry but never lands one in the wrong period.
Known gaps (not a backend's to close). A resource- or system-level change never reaches the bucket item (it propagates through the TTL). A lowered schedule there lets a refund land above the new ceiling until the next slow pass clamps it; a reset-schedule change there leaves the period guard following the lease's cron, so a reset under the new cron before the old edge can let the old period's refund land. An entity-level change reaches the item, and any reset-schedule change on the item drops the in-flight quota refunds (one per open lease per change).
Changed in v0.17.0: moving an entity (ADR-150)
A custom backend must implement set_parent, build_owner_check and
restamp_owner_if_current. The lease appends one build_owner_check item
(a condition-only transaction item that writes nothing) per owner whose bucket
a commit creates, after the bucket items and donor debits: it must fail the
transaction, with a per-item cancellation reason, when the owner's META no
longer holds the parent generation and incarnation the pass read (or now
exists when the pass read none). The lease calls restamp_owner_if_current
when the slow path's owner stamp lost its pin to another incarnation.
Changed in v0.17.0: soft limits and bypass (ADR-151)
A custom backend must implement build_bypass_consume,
resolve_bypass_from_fetched, resolve_soft_limits, limits_read_fresh,
_stamp_deadline, bypass_resource and bypass_entity, and accept the new
keywords below; the limiter and the lease pass them on every slow pass, so a
backend that does not raises TypeError there. fix_bucket_stamps is
optional: the limiter skips it on a backend without it.
- Stamps are deadlines. A bucket's
bypassandb_{name}_softhold an epoch-ms deadline.speculative_consumemust admit a declared limit without testing its balance only whileb_{name}_softis live and the item carries the limit, or whilebypassis live; fail an expired stamp asSCHEDULE_BOUNDARY(never a rejection); and reportbypassedon its result. A bypassed bucket is debited nothing but still countstcandwcu. build_composite_create(bypass_until_ms=...)stampsbypasswith that deadline; each state'ssoft/soft_until_msstampsb_{name}_soft.build_composite_normal(soft_stamps=, soft_reads=, soft_trusted=, bypass_stamp=): SET each stamp to its deadline or REMOVE it, each pinned on the value read (soft_reads, the second element ofbypass_stamp); waive a balance floor on a live soft stamp, and omit it for asoft_trustedlimit.build_composite_retry(soft_trusted=, now_ms=): the same waivers on the consumption-only retry, for its balance floors and its quota period guards; it never writes a stamp except a seed's.build_composite_adjust(tokens=False)writestconly (a bypassed lease's adjustment);counter=Falsewritestkonly (the bypass refund), still subject to the ceilings and period guards above.resolve_access(include_limits=, include_system=)returns each limit's soft-ness, read uncached, inConfigAccess.limit_soft;bypassis set when the decidingdisabledvalue is"bypass".get_resource_disabled/get_entity_disabledmay return"bypass", and the setters acceptdisabled="bypass": testdisabled is True, never its truthiness.
RepositoryProtocol
¶
Bases: Protocol
Protocol for rate limiter data backends.
All storage backends (DynamoDB, Redis, SQLite, In-Memory) must implement this protocol to work with RateLimiter. The protocol is divided into:
- Properties: Backend identification and configuration
- Lifecycle: Connection management
- Entity operations: CRUD for rate-limited entities
- Bucket operations: Token bucket state management
- Limit config: Stored limit configurations at entity/resource/system level
- Version management: Schema and Lambda version tracking
- Audit logging: Security audit trail
- Usage snapshots: Historical consumption tracking
Example
Custom backend implementation¶
class MyBackend: @property def region(self) -> str | None: return "us-east-1"
async def get_entity(self, entity_id: str) -> Entity | None:
...
Duck typing - no inheritance needed¶
repo = MyBackend() assert isinstance(repo, RepositoryProtocol) # True at runtime
capabilities
property
¶
Declare which extended features this backend supports.
Returns:
| Type | Description |
|---|---|
BackendCapabilities
|
BackendCapabilities instance with feature flags. |
Example
if repo.capabilities.supports_audit_logging: events = await repo.get_audit_events(entity_id)
namespace
async
¶
Return a scoped repository for the given namespace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Namespace name to resolve. |
required |
on_unavailable
|
OnUnavailableAction | None
|
Override on_unavailable for this namespace. |
None
|
bucket_ttl_multiplier
|
int | None
|
Override bucket TTL multiplier. |
None
|
Returns:
| Type | Description |
|---|---|
RepositoryProtocol
|
A repository scoped to the resolved namespace. |
Raises:
| Type | Description |
|---|---|
NamespaceNotFoundError
|
If the namespace is not registered. |
register_namespace
async
¶
Register a namespace (idempotent). Returns the namespace_id.
register_namespaces
async
¶
Bulk-register namespaces. Returns {name: namespace_id}.
list_namespaces
async
¶
List active namespaces. Returns [{name, namespace_id, created_at}].
recover_namespace
async
¶
Recover a deleted namespace. Returns the namespace name.
list_orphan_namespaces
async
¶
List deleted namespaces. Returns [{namespace_id, namespace, deleted_at}].
purge_namespace
async
¶
Purge all data for a deleted namespace. No-op if not found.
close
async
¶
Close the backend connection and release resources.
Must be called when the repository is no longer needed. Safe to call multiple times.
ping
async
¶
Check if the backend is reachable.
Returns:
| Type | Description |
|---|---|
bool
|
True if the backend is accessible, False otherwise. |
ensure_infrastructure
async
¶
Ensure backend infrastructure exists.
For DynamoDB: Creates CloudFormation stack if stack_options was provided to the constructor. For other backends: May create required resources or be a no-op.
Uses the options passed to the constructor. No-op if not provided.
create_entity
async
¶
Create a new entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Unique identifier for the entity |
required |
name
|
str | None
|
Human-readable name (defaults to entity_id) |
None
|
parent_id
|
str | None
|
Parent entity ID for hierarchical limits |
None
|
cascade
|
bool
|
If True, acquire() will also consume from parent entity |
False
|
metadata
|
dict[str, str] | None
|
Additional key-value metadata |
None
|
principal
|
str | None
|
Caller identity for audit logging |
None
|
Returns:
| Type | Description |
|---|---|
Entity
|
The created Entity |
Raises:
| Type | Description |
|---|---|
EntityExistsError
|
If entity already exists |
get_entity
async
¶
Get an entity by ID.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity identifier |
required |
consistent_read
|
bool
|
Read strongly consistently, for a caller that must see a write it just made |
False
|
Returns:
| Type | Description |
|---|---|
Entity | None
|
Entity if found, None otherwise |
delete_entity
async
¶
Delete an entity and all related records.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to delete |
required |
principal
|
str | None
|
Caller identity for audit logging |
None
|
Raises:
| Type | Description |
|---|---|
RateLimiterUnavailable
|
The backend could not delete every record.
What was deleted is audited, and calling |
get_children
async
¶
Get all child entities of a parent.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
parent_id
|
str
|
Parent entity ID |
required |
Returns:
| Type | Description |
|---|---|
list[Entity]
|
List of child entities |
set_parent
async
¶
Move an entity to a new parent, or to no parent (ADR-150).
Updates the entity's parent and makes every one of its bucket items name the new parent, so the next acquire debits it wherever the cascade policy (ADR-146) says the entity cascades. Consumption already charged to the old parent stays there.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to move. |
required |
parent_id
|
str | None
|
The new parent, which must exist, or None for no parent. |
required |
principal
|
str | None
|
Caller identity for audit logging. |
None
|
Returns:
| Type | Description |
|---|---|
int
|
Number of bucket items restamped. |
Raises:
| Type | Description |
|---|---|
ValidationError
|
The move would make the entity its own ancestor. |
EntityNotFoundError
|
The entity or the new parent does not exist. |
VersionMismatchError
|
The backend's readers predate parent moves. |
FanoutIncomplete
|
The move landed but restamping stopped part-way; repeating the call finishes it. |
restamp_owner_if_current
async
¶
Claim a bucket stamped by another incarnation, if this one is current (ADR-150).
The slow path's owner stamp only overwrites a stamp of its own
incarnation, because its META read is eventually consistent and may be
a deleted predecessor's. When that pin is lost to another incarnation,
one strongly consistent META read settles it: if incarnation is
current, the bucket is a predecessor's leftover and is restamped with
the current owner (cascade, parent_id, generation and
incarnation), never lowering a newer stamp; if not, it is left alone.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
The bucket's owner. |
required |
resource
|
str
|
The bucket's resource. |
required |
shard_id
|
int
|
The bucket's shard. |
required |
incarnation
|
str
|
The incarnation the caller's pass read. |
required |
Returns:
| Type | Description |
|---|---|
bool
|
Whether the bucket was restamped. |
get_bucket
async
¶
Get a token bucket by entity/resource/limit.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name (e.g., "gpt-4") |
required |
limit_name
|
str
|
Limit name (e.g., "tpm", "rpm") |
required |
Returns:
| Type | Description |
|---|---|
BucketState | None
|
BucketState if found, None otherwise |
get_buckets
async
¶
Get token buckets for an entity.
When resource is specified, fetches the bucket at the given shard_id. When resource is None, uses GSI3 to discover all buckets across resources and shards.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the buckets |
required |
resource
|
str | None
|
Resource name filter (None for all resources) |
None
|
shard_id
|
int
|
Shard to fetch when resource is specified |
0
|
Returns:
| Type | Description |
|---|---|
list[BucketState]
|
List of bucket states |
get_or_create_bucket
async
¶
Get an existing bucket or create a new one with the given limit.
This is the primary method for initializing token buckets. If a bucket exists, it is returned. If not, a new bucket is created with capacity set to the limit's capacity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name (e.g., "gpt-4") |
required |
limit
|
Limit
|
Limit configuration for the bucket |
required |
Returns:
| Type | Description |
|---|---|
BucketState
|
Existing or newly created BucketState |
batch_get_buckets
async
¶
Batch get composite buckets in a single call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
list[tuple[str, str, int]]
|
List of (entity_id, resource, shard_id) tuples. The shard is part of a bucket item's identity (GHSA-76rv); callers pass the shard the acquire selected rather than assuming shard 0. |
required |
Returns:
| Type | Description |
|---|---|
dict[tuple[str, str, str], BucketState]
|
Dict mapping (entity_id, resource, limit_name) to BucketState. |
batch_get_entity_and_buckets
async
¶
Fetch entity metadata and composite buckets in a single call.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity whose metadata to include |
required |
bucket_keys
|
list[tuple[str, str, int]]
|
List of (entity_id, resource, shard_id) for composite buckets |
required |
Returns:
| Type | Description |
|---|---|
tuple[Entity | None, dict[tuple[str, str, str], BucketState]]
|
Tuple of (entity_or_none, bucket_dict). |
select_shard
¶
Pick the bucket shard an acquire should target (GHSA-76rv, issue #439).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name |
required |
shard_id
|
int | None
|
Explicit shard to honour verbatim, or None to draw one at random from the shard_count |
None
|
shard_count
|
int | None
|
Count the caller observed (a speculative failure image), or None to read the entity cache |
None
|
avoid
|
frozenset[int]
|
Shards known not to fit the request (ADR-147); the draw is uniform over the others. Passed only when non-empty. |
frozenset()
|
Returns:
| Type | Description |
|---|---|
int
|
Tuple of (shard_id, shard_count) where shard_count is the given |
int
|
or cached value for this (entity, resource), defaulting to 1. |
get_resource_buckets
async
¶
Get all buckets for a resource across all entities.
Used for capacity reporting and aggregation.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
limit_name
|
str | None
|
Optional filter by limit name |
None
|
Returns:
| Type | Description |
|---|---|
list[BucketState]
|
List of bucket states |
build_bucket_put_item
¶
Build a transaction item for upserting a bucket.
This is a synchronous method used to build transaction payloads.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
state
|
BucketState
|
Bucket state to persist |
required |
ttl_seconds
|
int
|
Time-to-live for the record |
86400
|
Returns:
| Type | Description |
|---|---|
dict[str, Any]
|
Transaction item dict for use with transact_write |
build_owner_check
¶
Build the ConditionCheck that pins a bucket create to its owner's META (ADR-150).
A transaction item asserting the owner's META still holds the parent
generation and incarnation the create read, or is still absent when
exists is False. Rides in the create's own transaction, so a move
landing between the read and the create cancels it.
build_composite_create
¶
build_composite_create(entity_id, resource, states, now_ms, ttl_seconds=86400, cascade=False, parent_id=None, shard_id=0, shard_count=1, vu=None, rf_ms=None, bypass_until_ms=None, pgen=None, pinc=None)
Build a PutItem for creating a new composite bucket.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name |
required |
states
|
list[BucketState]
|
BucketState objects for each limit |
required |
now_ms
|
int
|
Current timestamp in milliseconds |
required |
ttl_seconds
|
int | None
|
TTL in seconds from now, or None to omit TTL |
86400
|
cascade
|
bool
|
Whether the entity has cascade enabled |
False
|
parent_id
|
str | None
|
The entity's parent_id (if any) |
None
|
vu
|
int | None
|
Valid-until stamp in epoch ms, or None to omit (#222 §2.1) |
None
|
rf_ms
|
int | None
|
The |
None
|
bypass_until_ms
|
int | None
|
The |
None
|
pgen
|
int | None
|
The owner's parent generation read with |
None
|
pinc
|
str | None
|
The owner's incarnation read with |
None
|
build_composite_normal
¶
build_composite_normal(entity_id, resource, consumed, refill_amounts, now_ms, expected_rf, ttl_seconds=None, shard_id=0, vu=None, clear_vu=False, windows=None, rf_ms=None, window_lengths=None, seeds=None, pin_shard_count=None, applied_windows=None, grant_counts=None, owner=None, pin_vu=False, expected_vu=None, soft_stamps=None, soft_trusted=frozenset(), owner_pgen=None, owner_pinc=None, read_tokens=None, cleared_top_ups=(), bypass_stamp=None, soft_reads=None)
Build an UpdateItem for the normal write path (ADR-115 path 2).
read_tokens (limit name -> tk as read) pins every limit whose
refill delta is positive on tk <= :read, so a credit landing
between the read and the write fails the write rather than having the
refill stack on it (#730).
owner is (cascade, parent_id) from the item owner's META, read
this pass: when given, both stamps are rewritten so the fast path's
denormalised copy is repaired (#684). None leaves them as stored.
owner_pgen is the owner's parent generation read with them
(ADR-150): stamped beside them, and the write is pinned on not
lowering it, so a pass that read META before a move cannot undo it.
owner_pinc is the owner's incarnation: the pin passes over a stamp
of another incarnation, whose generation is not comparable.
pin_vu: when the write sets or removes vu, require it to still be
expected_vu (absent when None), the value the caller read, so a
limit-change fan-out's vu = 0 stamped after the read survives (#701).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name |
required |
consumed
|
dict[str, int]
|
Amount consumed per limit (millitokens) |
required |
refill_amounts
|
dict[str, int]
|
Refill amount per limit (millitokens) |
required |
now_ms
|
int
|
Current timestamp in milliseconds |
required |
expected_rf
|
int
|
Expected refill timestamp for optimistic lock |
required |
ttl_seconds
|
int | None
|
TTL behavior (None=no change, 0=remove, >0=set) |
None
|
vu
|
int | None
|
Valid-until stamp in epoch ms, or None to leave it untouched |
None
|
clear_vu
|
bool
|
REMOVE |
False
|
windows
|
dict[str, tuple[int, int]] | None
|
Limit name -> |
None
|
rf_ms
|
int | None
|
The |
None
|
window_lengths
|
dict[str, int] | None
|
Limit name -> |
None
|
seeds
|
dict[str, BucketState] | None
|
Limit name -> state of a limit missing from this existing
item (#633), SET in full on this write under
|
None
|
pin_shard_count
|
int | None
|
The count a quota seed's share, or a reset or
roll's grant, was sized for; pins
|
None
|
applied_windows
|
dict[str, int] | None
|
Limit name -> the window start the write leaves
that limit's balance reflecting, stamped as |
None
|
grant_counts
|
dict[str, int] | None
|
Limit name -> the shard count a quota reset or roll
on this write re-granted it at, stamped as |
None
|
bypass_stamp
|
tuple[int | None, int | None] | None
|
|
None
|
soft_stamps
|
dict[str, int | None] | None
|
Limit name -> the |
None
|
soft_reads
|
dict[str, int | None] | None
|
Limit name -> the soft deadline read off the item
(None when absent), pinning each stamp write in
|
None
|
build_composite_retry
¶
build_composite_retry(entity_id, resource, consumed, shard_id=0, seeds=None, soft_trusted=frozenset(), period_guards=None, now_ms=None)
Build an UpdateItem for the retry write path (ADR-115 path 3).
period_guards (limit name -> (reset_edge_ms, window_start_ms)):
for each quota the lost write was to reset, roll or open, the retry
requires that period to be applied already (rf >= edge, or
b_{name}_wa = window_start), so it never spends the previous
period's balance (#743).
Must ask for ReturnValuesOnConditionCheckFailure=ALL_OLD: the lease
reports a failure here from the item's real balance (#633).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name |
required |
consumed
|
dict[str, int]
|
Amount consumed per limit (millitokens) |
required |
seeds
|
dict[str, BucketState] | None
|
Limit name -> state of a limit still missing from the item
(#633), written with |
None
|
now_ms
|
int | None
|
The instant a soft stamp's deadline is judged against (#467, ADR-151): a balance floor is waived only by a stamp live at it. None reads the clock. |
None
|
build_composite_adjust
¶
build_composite_adjust(entity_id, resource, deltas, shard_id=0, ceilings=None, trim=False, pin=None, *, tokens=True, counter=True)
Build an UpdateItem for the adjust write path (ADR-115 path 4).
An ADD for post-hoc correction. Can go negative by design.
tokens=False writes tc only (a bypassed lease, #311);
counter=False writes tk only (a bypass refund).
Positive delta = consumed more (subtract tokens, add to counter).
Negative delta = consumed less (add tokens, subtract from counter).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name |
required |
deltas
|
dict[str, int]
|
Delta per limit (millitokens, positive=consume, negative=release) |
required |
ceilings
|
dict[str, CreditCeiling] | None
|
Limit name -> |
None
|
trim
|
bool
|
Build the fallback: the same unconditional ADD plus
|
False
|
pin
|
CreditPin | None
|
The item's ( |
None
|
transact_write
async
¶
Execute a write of one or more items.
Single-item batches use the corresponding single-item API (PutItem, UpdateItem, or DeleteItem) to halve WCU cost. Multi-item batches use TransactWriteItems for atomicity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
list[dict[str, Any]]
|
List of transaction items from build_bucket_put_item |
required |
client_request_token
|
str | None
|
Idempotency token for a multi-item transaction (ADR-149); ignored for a single item. |
None
|
Returns:
| Type | Description |
|---|---|
dict[str, Any] | None
|
For a single-item update, the item's attributes after the write |
dict[str, Any] | None
|
( |
dict[str, Any] | None
|
a backend cannot say, |
Raises:
| Type | Description |
|---|---|
TransactionCanceledException
|
If multi-item transaction fails |
ConditionalCheckFailedException
|
If single-item condition fails |
write_each
async
¶
Write items independently without cross-item atomicity.
Each item is dispatched as a single PutItem, UpdateItem, or DeleteItem call (1 WCU each). Use for unconditional writes (e.g., ADD adjustments) where partial success is acceptable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
items
|
list[dict[str, Any]]
|
List of items to write independently |
required |
Returns:
| Type | Description |
|---|---|
list[dict[str, Any]] | None
|
One entry per item: the attributes an UpdateItem returned (when the |
list[dict[str, Any]] | None
|
item asked for |
list[dict[str, Any]] | None
|
return |
speculative_consume
async
¶
speculative_consume(entity_id, resource, consume, ttl_seconds=None, shard_id=None, now_ms=None, avoid_shards=frozenset(), avoid_parent_shards=frozenset())
Attempt speculative UpdateItem with condition check.
Issues an UpdateItem with ADD -consumed and condition
attribute_exists(PK) AND tk >= consumed for each limit.
Uses ReturnValuesOnConditionCheckFailure=ALL_OLD to return
the current item state on failure.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name |
required |
consume
|
dict[str, int]
|
Amount per limit (tokens, not milli) |
required |
ttl_seconds
|
int | None
|
TTL in seconds from now, or None for no TTL change |
None
|
shard_id
|
int | None
|
Explicit shard to target (skips random selection and cascade logic). None means auto-select from entity cache. |
None
|
now_ms
|
int | None
|
The caller's "now" (issue #430), so one logical
|
None
|
avoid_shards
|
frozenset[int]
|
Shards the caller knows cannot fit the request (ADR-147); the shard is drawn among the others. Passed only when non-empty, and only by a limiter whose repository keeps a rejection cache, so a backend without one never receives it. |
frozenset()
|
avoid_parent_shards
|
frozenset[int]
|
The same for the parent's shards on a parallel cascade write (ADR-147 phase 2), on the same terms. |
frozenset()
|
Returns:
| Type | Description |
|---|---|
SpeculativeResult
|
SpeculativeResult with success flag and either: |
SpeculativeResult
|
|
SpeculativeResult
|
|
SpeculativeResult
|
|
bump_shard_count
async
¶
Double shard_count on shard 0 via conditional write.
Also updates the entity cache with the new shard_count.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name |
required |
current_count
|
int
|
Expected current shard_count |
required |
Returns:
| Type | Description |
|---|---|
int
|
The new shard_count, or current_count if another client already doubled. |
plan_quota_shard
async
¶
Plan the grant of every quota in limits for shard shard_id (ADR-145).
Reads the siblings once and decides per quota a move off the
current-period sibling covering this slot, or a fresh grant when none
does (models.plan_quota_grant). The count is the largest of
shard_count and every sibling's stored count; a lagging sibling
has it raised before a fresh grant (design §8 R5). Writes nothing
else: the debits ride in the acquire's own transaction.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the shards |
required |
resource
|
str
|
Resource the shards belong to |
required |
limits
|
Sequence[Limit]
|
The limits being created or seeded; non-quotas are ignored |
required |
shard_id
|
int
|
The shard being created or seeded |
required |
shard_count
|
int
|
The caller's shard count |
required |
now_ms
|
int
|
The pass's clock reading |
required |
Returns:
| Type | Description |
|---|---|
int
|
|
dict[str, QuotaGrant]
|
the grants takes its full share. |
repair_created_quota_shard
async
¶
Raise a just-created quota shard whose count a doubling overtook (ADR-145).
One strongly consistent read of shard 0's shard_count; when it is
higher than created_count, the created item's count is raised with
its current grant size frozen (gc = if_not_exists(gc, :old)).
Returns:
| Type | Description |
|---|---|
bool
|
True when the created item was raised. |
build_quota_donor_debits
¶
The donor side of each ADR-145 move, one Update per donor shard.
Conditioned on the donor still holding the tokens, still at the grant
count read (or carrying none and at that shard count), and its grant
still in the current period. Several quotas moving off one donor
share one Update.
get_shard_window_starts
async
¶
Read one shard's duration-window starts, to seed a shard being created (ADR-140).
A shard created mid-window joins the window in progress rather than
opening its own, so the create path reads the ws it would inherit
from shard 0 — the source of truth for shard_count already.
The read must be strongly consistent: a stale pre-roll ws looks
ended, and the caller would then grant the new shard a fresh full share
instead of the ADR-145 move from the window shard 0 just opened.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity whose shard is being created. On a cascade create this is the parent for the parent's shard, never the child. |
required |
resource
|
str
|
Resource name |
required |
limit_names
|
list[str]
|
The limits to look for |
required |
shard_id
|
int
|
The shard to read. Defaults to 0. |
0
|
Returns:
| Type | Description |
|---|---|
dict[str, int]
|
|
dict[str, int]
|
has no window on that shard, or the shard does not exist. |
reconcile_bucket_to_defaults
async
¶
Reconcile bucket to effective defaults after config deletion (issue #327).
Updates limit fields to match effective limits, sets TTL, and removes stale limit attributes. No-op if bucket doesn't exist.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity owning the bucket |
required |
resource
|
str
|
Resource name |
required |
effective_limits
|
list[Limit]
|
New effective limits (resource/system defaults) |
required |
stale_limit_names
|
set[str] | None
|
Limit names to REMOVE from bucket |
None
|
set_limits
async
¶
set_limits(entity_id, limits, resource='_default_', principal=None, *, disabled=PRESERVE_DISABLED, cascade=PRESERVE_CASCADE)
Store limit configs for an entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to configure |
required |
limits
|
list[Limit]
|
Limit configurations |
required |
resource
|
str
|
Resource these limits apply to |
'_default_'
|
principal
|
str | None
|
Caller identity for audit logging |
None
|
cascade
|
bool | None
|
Tri-state cascade policy (ADR-146); defaults to preserving the stored value. An explicit value fans out to buckets. |
PRESERVE_CASCADE
|
get_limits
async
¶
Get stored limit configs for an entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to query |
required |
resource
|
str
|
Resource to get limits for |
'_default_'
|
Returns:
| Type | Description |
|---|---|
list[Limit]
|
List of limits (empty if none configured) |
delete_limits
async
¶
Delete stored limit configs for an entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to delete limits for |
required |
resource
|
str
|
Resource to delete limits for |
'_default_'
|
principal
|
str | None
|
Caller identity for audit logging |
None
|
list_entities_with_custom_limits
async
¶
List all entities that have custom limit configurations for a resource.
Uses GSI3 sparse index for efficient queries.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource to filter by (required). |
required |
limit
|
int | None
|
Maximum number of entities to return. None for all. |
None
|
cursor
|
str | None
|
Pagination cursor from previous call. None for first page. |
None
|
Returns:
| Type | Description |
|---|---|
tuple[list[str], str | None]
|
Tuple of (entity_ids, next_cursor). next_cursor is None if no more results. |
list_resources_with_entity_configs
async
¶
List all resources that have entity-level custom limit configurations.
Uses the entity config resources registry for efficient O(1) lookup.
Returns:
| Type | Description |
|---|---|
list[str]
|
Sorted list of resource names with at least one entity having custom limits |
set_resource_defaults
async
¶
set_resource_defaults(resource, limits, principal=None, *, disabled=PRESERVE_DISABLED, cascade=PRESERVE_CASCADE)
Store default limits for a resource.
Resource defaults apply to all entities accessing this resource unless overridden at the entity level.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
limits
|
list[Limit]
|
Default limits |
required |
principal
|
str | None
|
Caller identity for audit logging |
None
|
get_resource_defaults
async
¶
Get default limits for a resource.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
Returns:
| Type | Description |
|---|---|
list[Limit]
|
List of limits (empty if none configured) |
delete_resource_defaults
async
¶
Delete default limits for a resource.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
resource
|
str
|
Resource name |
required |
principal
|
str | None
|
Caller identity for audit logging |
None
|
list_resources_with_defaults
async
¶
List all resources that have default limits configured.
Returns:
| Type | Description |
|---|---|
list[str]
|
List of resource names |
set_system_defaults
async
¶
Store system-wide default limits and config.
System defaults apply to ALL resources unless overridden.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
limits
|
list[Limit]
|
Global default limits |
required |
on_unavailable
|
OnUnavailableAction | None
|
Behavior when backend unavailable ("allow" or "block") |
None
|
principal
|
str | None
|
Caller identity for audit logging |
None
|
get_system_defaults
async
¶
Get system-wide default limits and config.
Returns:
| Type | Description |
|---|---|
tuple[list[Limit], OnUnavailableAction | None]
|
Tuple of (limits, on_unavailable). on_unavailable may be None. |
delete_system_defaults
async
¶
Delete all system-wide defaults.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
principal
|
str | None
|
Caller identity for audit logging |
None
|
get_version_record
async
¶
Get the infrastructure version record.
Returns:
| Type | Description |
|---|---|
dict[str, Any] | None
|
Version record with schema_version, lambda_version, etc. |
dict[str, Any] | None
|
None if no version record exists. |
require_reset_after_readers
async
¶
Refuse reset_after limits the stack's Lambdas cannot read (#638).
Called by acquire(limits=...) before anything is written. Free when
no limit carries reset_after, and free when the backend already
knows its readers are new enough.
Raises:
| Type | Description |
|---|---|
VersionMismatchError
|
the readers predate |
set_version_record
async
¶
Set the infrastructure version record.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
schema_version
|
str
|
Current schema version (e.g., "1.0.0") |
required |
lambda_version
|
str | None
|
Currently deployed Lambda version |
None
|
client_min_version
|
str | None
|
Minimum compatible client version. |
None
|
updated_by
|
str | None
|
Identifier of what performed the update |
None
|
get_audit_events
async
¶
Get audit events for an entity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to query |
required |
limit
|
int
|
Maximum events to return |
100
|
start_event_id
|
str | None
|
Pagination cursor (event ID to start after) |
None
|
Returns:
| Type | Description |
|---|---|
list[AuditEvent]
|
List of audit events, most recent first |
get_usage_snapshots
async
¶
get_usage_snapshots(entity_id=None, resource=None, window_type=None, start_time=None, end_time=None, limit=100, next_key=None)
Query usage snapshots with filtering and pagination.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str | None
|
Entity to query (uses primary key) |
None
|
resource
|
str | None
|
Resource filter (required if entity_id is None) |
None
|
window_type
|
str | None
|
Filter by "hourly" or "daily" |
None
|
start_time
|
str | None
|
Filter snapshots >= this timestamp (ISO format) |
None
|
end_time
|
str | None
|
Filter snapshots <= this timestamp (ISO format) |
None
|
limit
|
int
|
Maximum items per page |
100
|
next_key
|
dict[str, Any] | None
|
Pagination cursor from previous call |
None
|
Returns:
| Type | Description |
|---|---|
tuple[list[UsageSnapshot], dict[str, Any] | None]
|
Tuple of (snapshots, next_key). next_key is None if no more results. |
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither entity_id nor resource is provided |
get_usage_summary
async
¶
Aggregate usage across snapshots into a summary.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str | None
|
Entity to query |
None
|
resource
|
str | None
|
Resource filter |
None
|
window_type
|
str | None
|
Filter by "hourly" or "daily" |
None
|
start_time
|
str | None
|
Filter snapshots >= this timestamp |
None
|
end_time
|
str | None
|
Filter snapshots <= this timestamp |
None
|
Returns:
| Type | Description |
|---|---|
UsageSummary
|
UsageSummary with total, average, and time range |
Raises:
| Type | Description |
|---|---|
ValueError
|
If neither entity_id nor resource is provided |
resolve_limits
async
¶
Resolve effective limits using the four-level config hierarchy.
Resolution order (first non-empty wins):
1. Entity-level config for this specific resource
2. Entity-level _default_ config (fallback for all resources)
3. Resource-level defaults
4. System-level defaults
Backend implementations should use native caching and resolution strategies. For DynamoDB, this uses ConfigCache with batched fetch optimization.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
entity_id
|
str
|
Entity to resolve limits for |
required |
resource
|
str
|
Resource being accessed |
required |
disabled_out
|
dict[tuple[str, str], bool | str | None] | None
|
Optional dict receiving the tri-state |
None
|
cascade_out
|
dict[tuple[str, str], bool | str | None] | None
|
The same, for the tri-state cascade policy (ADR-146);
see |
None
|
Returns:
| Type | Description |
|---|---|
list[Limit] | None
|
Tuple of (limits, on_unavailable, config_source) where: |
OnUnavailableAction | None
|
|
ConfigSource | None
|
|
tuple[list[Limit] | None, OnUnavailableAction | None, ConfigSource | None]
|
|
resolve_disabled
async
¶
Resolve the effective disabled state for an entity+resource (ADR-125).
resolve_access
async
¶
resolve_access(entity_id, resource, *, consistent_read=False, include_limits=False, include_system=False)
Resolve disabled and the cascade policy from one uncached read (ADR-146).
Both walks cover the same config items, so a backend answers them from
one read. Never from cache, for the reasons resolve_disabled gives.
consistent_read is for a caller resolving a level it just wrote.
include_limits also returns each resolved limit's soft-ness in
ConfigAccess.limit_soft from the same read (#467);
include_system adds the system level to it.
limits_read_fresh
¶
Whether one config fetch read every level that decided the limits (#467).
fetched is the disabled_out of that resolve_limits call. A
fresh read is the authority the slow path re-stamps soft-ness from.
resolve_soft_limits
async
¶
Each resolved limit's soft-ness, read uncached (#467, ADR-151 §6.2).
Used when the slow path creates or seeds a bucket from limits the config cache served: a created bucket's soft stamp must never be stale.
resolve_cascade_from_fetched
¶
Answer the cascade-policy walk from a config fetch, or decline (ADR-146).
Returns (policy, deciding_level), policy None when no level sets
it; or None when any level is absent from fetched, meaning the
caller must fall back to resolve_access.
get_entity_disabled
async
¶
Read the tri-state disabled flag from an entity config item (ADR-125).
Returns True/False when set explicitly, None when unset (inherit).
In the contract because cli.py reads it directly.
get_resource_disabled
async
¶
Read the tri-state disabled flag from a resource config item (ADR-125).
Returns True/False when set explicitly, None when unset (inherit).
In the contract because cli.py reads it directly.
resolve_disabled_from_fetched
¶
Answer the disable walk from a config fetch, or decline (ADR-125).
Returns None when any level of the walk is absent from fetched,
meaning the caller must fall back to resolve_disabled. A backend
must never answer this gate from cached config: with no bucket yet,
the fast-path guard cannot fire, so a stale value would admit an
entity to a disabled resource and leave behind an unstamped bucket
that the completed fan-out will never revisit.
build_bypass_consume
¶
The slow path's tc-only write for a bypassed bucket (#311).
Conditioned on the item carrying a bypass stamp (any deadline), so a
bypass cleared since the pass read config fails the write rather than
admitting unenforced. Renews the stamp to bypass_until_ms (ADR-151
decision 8).
fix_bucket_stamps
async
¶
Correct a bucket's stamps before a slow-path rejection (ADR-151, review E1).
SET or REMOVE bypass and each b_{name}_soft as config
read uncached says, each given as (deadline, read) and pinned on
read; nothing else is written. False when a pin no longer matched.
Optional: the limiter looks it up with getattr, so a backend
without it simply skips the correction.
resolve_bypass_from_fetched
¶
Whether the walk resolves to bypass, from a config fetch, or decline (#311).
disable_resource
async
¶
Disable a resource for all entities without an explicit override (ADR-125).
bypass_resource
async
¶
Bypass a resource: admit everything, debit nothing, count usage (#311).
enable_resource
async
¶
Explicitly enable a resource (stores disabled: false).
clear_resource_disabled
async
¶
Remove the resource's explicit disabled value, reverting to inherit.
disable_entity
async
¶
Disable an entity, for one resource or across all of them (ADR-125).
enable_entity
async
¶
Explicitly enable an entity, overriding a disabled resource.
bypass_entity
async
¶
Bypass an entity, for one resource or across all of them (#311).
clear_entity_disabled
async
¶
Remove the entity's explicit disabled value, reverting to inherit.
get_resource_cascade
async
¶
Read the tri-state cascade policy from a resource config item (ADR-146).
get_entity_cascade
async
¶
Read the tri-state cascade policy from an entity config item (ADR-146).
set_resource_cascade
async
¶
Set a resource's cascade policy and restamp its buckets (ADR-146).
clear_resource_cascade
async
¶
Remove the resource's cascade policy, reverting to inherit (ADR-146).
set_entity_cascade
async
¶
Set an entity's cascade policy, for one resource or all (ADR-146).
clear_entity_cascade
async
¶
Remove the entity's cascade policy, reverting to inherit (ADR-146).
reset_bucket
async
¶
Restore an entity's balance on one resource to its full share, now (ADR-149).
Every shard, one rf-locked zero-consumption pass, one transaction; the named limits (all when None) back to their full share and a new period.
top_up
async
¶
Add whole tokens per limit to an entity's balance on one resource, now (ADR-149).
A quota gains exactly the amount (above its plan for this period, behind the 0.17.0 version gate); a dripping limit at most up to its ceiling.
resolve_on_unavailable
async
¶
Resolve on_unavailable from system config, with caching fallback.
Returns the on_unavailable action. Caches the value after first successful load for use as fallback when DynamoDB is unreachable. Defaults to "block" if no system config and no cached value.
invalidate_config_cache
async
¶
Invalidate all cached config entries.
Backends with caching should evict all entries. Backends without caching (e.g., Redis with native TTL) should no-op.
Call after modifying config to force immediate refresh.
get_cache_stats
¶
Get config cache performance statistics.
Returns statistics for monitoring cache behavior: - hits: Cache hits (avoided backend reads) - misses: Cache misses (backend reads performed) - size: Current entry count - ttl: TTL in seconds
Backends without caching should return zero-valued stats.
Returns:
| Type | Description |
|---|---|
CacheStats
|
CacheStats object with cache metrics |