feat: quota deployments per entity type and client address - #1965
Open
LautaroPetaccio wants to merge 1 commit into
Open
feat: quota deployments per entity type and client address#1965LautaroPetaccio wants to merge 1 commit into
LautaroPetaccio wants to merge 1 commit into
Conversation
POST /entities had no bound on how much one client can deploy over time. POST_ENTITIES_RATE_LIMIT_* is a request budget over a single 60s window and is blind to entity type; DEPLOYMENT_RATE_LIMIT_* throttles redeployments of one pointer, not a caller's total. So one address could deploy 200 distinct scenes a minute indefinitely. Adds a deployment-quota logic component bounding attempts per (client address, entity type) over a minute, hour, day and week, enforced in the POST /entities handler — the entity type lives inside the uploaded entity file, so it cannot be known before the multipart parse. The entity is located by computed hash, since the multipart field names are the caller's to choose. Counting is against the component's own cache rather than IRateLimiterComponent.consume, which rejects any window longer than a day; the window arithmetic, key layout and address resolution are still that package's exported ones. Defaults to 60/600/3000/10000 so the server runs unconfigured, with per-entity-type overrides and DEPLOYMENT_QUOTA_EXEMPT_IPS for a backend deploying for many users from one address. A zero, an unknown entity type, a malformed address and a ladder whose longer window is tighter than a shorter one all fail startup.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
POST /entitieshad no bound on how much a single client can deploy over time. The two guards that look like they cover it do not:POST_ENTITIES_RATE_LIMIT_*is a per-client request budget over one 60s window and is blind to entity type, andDEPLOYMENT_RATE_LIMIT_*throttles redeployments of the same pointer rather than one caller's total. One address could deploy 200 distinct scenes a minute indefinitely with every guard reading healthy.What
A
deployment-quotalogic component bounding deploy attempts per (client address, entity type) over a minute, hour, day and week. It is enforced in thePOST /entitieshandler rather than at its mount, because the entity type lives inside the uploaded entity file and cannot be known before the multipart parse. The entity is located by its computed hash — the multipart field names are the caller's to choose, so keying on them would let anyone skip the quota by renaming the entity file. The resulting hash map is handed todeployEntity, so nothing is hashed twice.Every attempt counts, including one that later fails validation, so a client sending garbage cannot deploy for free. A rejection answers
429withRetry-Afterand says only when to retry, matching theRateLimitDisclosure.RETRY_AFTERconvention the rest of the fleet follows.Defaults are 60/600/3000/10000 so the server runs unconfigured, with
DEPLOYMENT_QUOTA_MAX_PER_{WINDOW}_{ENTITY_TYPE}overrides andDEPLOYMENT_QUOTA_EXEMPT_IPS(addresses and CIDRs) for a backend deploying on behalf of many users from one address. A zero, an unknown entity type, a malformed address, and a ladder whose longer window is tighter than a shorter one all fail startup rather than run misconfigured.Note for review
The week tier cannot go through
IRateLimiterComponent.consume:MAX_WINDOW_SECONDS = 86400is enforced inresolvePolicy, which bothconsume()and the middleware go through, as a seconds/milliseconds mix-up guard. Rather than weaken that guard in a shared package for one consumer,counter.tscounts against the cache directly while reusing the package's exportedwindowOffsetFor,currentWindow,buildCounterKey,encodeIdentity,FALLBACK_IDENTITYand both address helpers — so the window arithmetic, key layout and proxy-hop handling stay shared, and only the increment, the fail-open policy and the fallback divisor are owned here. The cost is that quota decisions no longer land onrate_limiter_requests_total;dcl_content_deployment_quota_attempts_total{entity_type, window, outcome}carries them instead, withallowed/limited/degraded.Two things worth a second opinion:
countAttemptlevel instead.Counters are in memory, so a restart resets them. The limiter takes any cache with
increment, so a durable store can be swapped in later without touching quota logic.Verification
yarn build, lint and prettier clean. Unit 463/463 across 40 suites; integration 37/37 suites, 323 passed, with no quota rejection leaking into an existing spec. Local integration ran the way CI does (CI=truewith an external postgres) because testcontainers/ryuk hangs against Docker 29.4.3 on this machine — unrelated to these changes, but the local testcontainers bootstrap path was not exercised.