RFDs/0021

Per-organization limits

State
discussion
Authors
Darwin D Wu
Discussion
renga-dcim/renga#174
Labels
rengaoperationsorganizationsperformancesoftware
Implemented
0%Checklist

Goal

Keep one organization from degrading Renga for the others in a deployment that serves several. Today nothing stops one organization’s agents, people, or background work from taking the whole database:

  • Every resource write in every organization takes one global advisory lock to allocate its revision (Renga.Inventory.ResourceStore.next_revision!/0). A burst of intake from one organization queues every other organization’s writes behind it.

  • Intake runs synchronously in the request transaction with no per-organization cap, so one organization’s agents can hold most of the database connection pool.

  • Background sweeps walk organizations one after another with no time budget, so one large organization delays the rest.

  • The only per-organization limit is job creation, at 10,000 jobs a minute.

A deployment that serves one organization keeps today’s behavior: the default limits are generous, and the lock change helps it too.

Removing global contention

Revision allocation

Revision allocation holds a lock until commit so that anyone reading changes in revision order never skips a revision that is allocated but not yet visible. Every such reader is scoped to one organization, so the order only has to hold within an organization. The lock becomes per organization, keyed by the organization ID in a resource-revision-specific advisory-lock namespace. The revision sequence stays global, so revisions remain unique and increase within each organization; they no longer say anything about order across organizations, which nothing relies on.

Old writers taking only the global lock cannot run alongside writers taking the new lock: they could commit revisions for one organization out of order. The deployment that enables the new allocator therefore stops and drains all resource writers, including background work, deploys the new allocator to every node, and only then resumes writes. A future rolling cutover would need a staged dual-lock protocol instead.

Other locks

Renga’s other advisory locks are already keyed by organization, alone or together with an installation, a source, or the hierarchy being changed. New code follows the same rule: a lock that serializes work across organizations needs a reason in its RFD.

Request limits

Each organization gets rate limits. Intake admission has two bounded stages so traffic cannot exhaust the database pool before Renga learns which organization owns it. A small node-wide authentication lane bounds credential lookups for valid and invalid callers. After lookup, but before any domain write, the organization and installation limits apply. Latest authenticated contact still includes rejected authenticated requests as RFD 1 requires, but contact writes are coalesced by installation through a bounded recorder rather than issuing a database update for every rejected request.

  • Intake: observations, check-ins, job leases, and job events are limited per organization and per installation. Bulk observation traffic has its own capacity. Check-ins and reports for already-leased jobs have reserved capacity, and Renga stops granting new leases before that reserve is needed. This keeps lease maintenance available when observations are throttled. Exhausted capacity returns 429 with Retry-After.

  • Intake size: the largest observation an organization may send.

  • Concurrency: at most a set number of admitted intake operations per organization run at once on each node, a fraction of the connection pool by default. Admission is held through the complete synchronous operation, including observation acceptance and inline reconciliation even though they use separate transactions. Extra requests get 429 rather than waiting for a connection.

  • People and API: HTTP requests, authenticated LiveView navigations, and LiveView events are limited per user and per organization, high enough that nobody working normally meets them. A throttled LiveView event does not run its handler and shows a retryable notice; an HTTP request gets 429.

Retry-After is a retry-not-before deadline, not permission to exceed an agent’s delivery budget. If the deadline and another request cannot fit in the current 25-second budget, the agent returns control and schedules a later attempt instead of sleeping past the budget. It keeps observations and terminal job outcomes durable until delivery. If overload nevertheless lets a job lease expire, Renga applies RFD 9's existing retry-class transition and records lease_lost; it never treats an unreported outcome as delivered.

Job creation keeps its existing limit, which becomes one of these settings.

Database time

Every application database connection has a baseline statement_timeout, so standalone reads and writes are covered as well as explicit transactions. Requests use the shorter application default. Approved background workers may set a longer transaction-local timeout for their own queries; work such as a future export (RFD 20) runs there. Because statement_timeout bounds each statement rather than a whole job, every worker also has a wall-clock budget and a resumable boundary.

Background work

Sweeps process one bounded batch per organization, then move to the next organization and return to unfinished work on a later round. The scheduler persists its cursor, and an organization cannot receive a second batch until the other ready organizations have received one. One organization’s failure is logged and skipped for that round rather than delaying the rest.

Neighbor expiry needs a stronger boundary than batching expired evidence. Adjacencies, neighbor findings, and cable findings are complete-set reconciliations: publishing a result computed from a partial scan could delete valid topology or resolve unrelated findings. Each run therefore captures a fixed evidence watermark and as_of time, builds all derived topology into a new organization-scoped generation in bounded, resumable batches, and exposes none of it until the generation is complete. Final publication takes the organization topology lock, verifies that the relevant evidence version has not changed, and atomically switches the current generation; otherwise the run is discarded and restarted. Old generations are purged separately in bounded batches. Unvisited evidence is never interpreted as absent.

The existing job lease/deadline sweep joins history purge in the fair organization rotation. Lease and deadline batches keep their per-job SKIP LOCKED transactions, and the rotation’s maximum scheduling delay stays inside the lease-expiry handling budget. Future background work uses the same fair scheduler, or a queue with the same one-batch-per-organization property, rather than draining one organization before starting the next.

Quotas

An organization has hard quotas with these accounting rules:

Quota Capacity held Capacity released

Resources

One unit for every persisted resource envelope, in every lifecycle state

When the envelope is physically deleted, not when it is retired or deletion is requested

Agents

One unit for every persisted agent, active or disabled

When the agent row is physically deleted

Installation identities (RFD 13)

One unit for every persisted pending, approved, or disabled identity

When the identity is physically deleted

Intake keys

One unit for every active key

When the key is revoked

Terminal recordings (RFD 13)

Stored bytes plus the unused part of each recorded active session’s 50 MiB reservation

When retained bytes are deleted, or unused reservation is released at recorded session end

Export archives (future RFD 20)

Stored bytes plus the unused part of each running export’s declared maximum reservation

When an archive is deleted, or unused reservation is released when generation ends

Every capacity acquisition uses a persisted organization quota ledger. The same database transaction locks the ledger, checks or creates an idempotent reservation, and performs the creation, so creators on different nodes cannot both take the last unit. Long-lived byte reservations survive process restarts, have an owner and expiry, and are reclaimed only after the owner is terminal. Writing bytes converts reserved capacity to used capacity without increasing the total. When the effective policy at session start requires recording, a terminal is not authorized or started unless its full recording reservation succeeds. That start-time decision and reservation remain fixed for the running session if the setting later changes. An unrecorded session acquires no recording reservation, but retains RFD 13's required Activity record. An export similarly does not start without its maximum reservation.

At 80% of a quota Renga opens a finding for the organization’s owners. At the quota, Renga refuses an operation that needs more capacity and says which quota stopped it. It never deletes data automatically. Updates remain allowed only when they do not increase quota usage or when an existing reservation covers the increase. Lowering a limit below current usage opens the finding immediately and blocks new reservations, while existing reservations, non-growing updates, and deletions continue.

When intake matches an existing resource, reconciliation continues because no new resource unit is needed. When it would create a resource without capacity, Renga stores the immutable observation and records a failed reconciliation whose structured error is quota_deferred, preserving the existing accepted response schema. In the same transaction it inserts one durable retry item per observation. A bounded worker schedules those items fairly by organization and uses the explicit reconciliation retry path, not reconcile_once, after a resource is deleted or the limit increases; a periodic pass covers missed wakeups and restarts. It removes an item only after a non-quota terminal result. Retries rerun normal identity matching, idempotency, and freshness ordering, so an older deferred observation cannot overwrite newer reconciled evidence. The quota finding lists the durable items waiting for capacity.

Configuration

Every limit and quota has a default, which an operator sets for the deployment through the environment. This RFD does not fix the numbers. The built-in defaults must satisfy one rule: a deployment serving a single organization, under the realistic intake of a large site (thousands of hosts reporting on their normal schedule, plus a full fleet re-enrollment), never meets any of them. The implementation establishes the defaults with a load test that it documents alongside the configuration reference, and revises them the same way. Concurrency defaults are expressed relative to the connection pool, so they follow the deployment’s size. An operator can override them for one organization with a release command, such as renga limits set --organization <slug> --intake-rate 600. Organization owners see their usage against each limit and quota on a read-only page in organization settings; they cannot change them.

Renga emits telemetry for intake requests, intake bytes, database time, and rejected requests, tagged with the organization ID, so an operator can see which organization is using what before it reaches a limit.

An organization that needs more than a shared deployment can give it can move to a deployment of its own with an export and import (RFD 20).

Decisions

  1. Default values. The RFD states the sizing rule and the required load test (Configuration) rather than numbers. Numbers picked before measurement would go stale; the rule holds whatever the numbers turn out to be.

Future work

  • Usage-based limits on stored history and raw observations.

  • Limits on saved-view and query cost once RFD 12 queries exist.