5. Concurrency
Overview
Section titled “Overview”requireWithin is check-then-act. Take two parallel requests, each creating the 5th project of a 5-project plan. Both count 4, both pass, and the organization ends up with 6. This spec cannot fix that inside requireWithin, because the act (inserting the project) happens in the application’s own storage, after the check returns.
The spec therefore separates two things:
requireWithin: a pure check, with no atomicity guarantee.guard: check-then-act, serialized per(organizationId, privilege)through a pluggable lock, with the count and the act running inside the lock.
Serialization depends on where the usage lives (usually the application’s database), not on where the limits come from. So the lock is a client option and not a provider capability. A Stripe adapter can supply the limits while a Postgres advisory lock serializes the inserts.
5.1. requireWithin is not atomic
Section titled “5.1. requireWithin is not atomic”Requirement 5.1.1
Section titled “Requirement 5.1.1”SDKs MUST document that
requireWithinalone does not prevent concurrent requests from exceeding a limit (not mechanically testable).
5.2. guard
Section titled “5.2. guard”await client.guard("projects.max", 1, ctx, { count: (tx) => tx.query("select count(*) from projects where org_id = $1", [orgId]), act: (tx) => tx.query("insert into projects ...", [...]),});Requirement 5.2.1
Section titled “Requirement 5.2.1”SDKs MUST provide
guard(privilegeKey, requested, context, {count, act}). Its steps are:
- acquire the lock for the key
(organizationId, privilegeKey),- resolve the privilege,
- call
count(tx)to getcurrent(an integer or a usage breakdown),- apply the
requireWithincheck,- if the check passes, call
act(tx),- release the lock.
When the check passes,
guardMUST return the result ofacttogether with the resolution details. When it fails,guardMUST throwEntitlementLimitExceededwithout callingact.
Requirement 5.2.2
Section titled “Requirement 5.2.2”For a fixed lock domain, concurrent
guardcalls on the same key MUST be linearizable. Their outcome MUST equal that of some serial execution of the same calls.
Conformance: with a limit of L and a usage counter starting at 0, N > L concurrent guards that each request 1 and increment the counter in act MUST result in exactly L successes, and the counter MUST end at L.
Requirement 5.2.3
Section titled “Requirement 5.2.3”The lock key MUST include
organizationIdandprivilegeKey. It MUST NOT includesubjectIdorresourceId.
A shared pool is shared across subjects, so locking per subject would let two users overdraw it together.
Requirement 5.2.4
Section titled “Requirement 5.2.4”The lock MUST be released when
count, the check, oractthrows, and the error MUST propagate to the caller.
5.3. Locks
Section titled “5.3. Locks”Requirement 5.3.1
Section titled “Requirement 5.3.1”A lock MUST implement
run(key, fn(tx)) → result. It MUST runfnwith exclusive ownership ofkeywithin its domain, and MUST pass the sametxhandle to bothcountandact. A transactional lock MUST runcountandactin the same transaction and MUST commit only afteractsucceeds.
Requirement 5.3.2
Section titled “Requirement 5.3.2”SDKs MUST ship an in-process lock and MUST document that it serializes only within one process. A client created without an explicit lock MUST use the in-process lock.
Requirement 5.3.3
Section titled “Requirement 5.3.3”A lock MUST declare its
domain(for exampleprocessordatabase). Resolution details returned fromguardSHOULD carry it, so operators can tell when a deployment runs on process-local locks.
5.4. Reference: Postgres
Section titled “5.4. Reference: Postgres”(informative) The reference Postgres lock opens a transaction, takes pg_advisory_xact_lock(hashtextextended('oe:' || organization_id || ':' || privilege, 0)), runs count and act on the same connection, and commits. The advisory lock is released at commit or rollback. Hash collisions only cause extra serialization, never incorrect results. Alternatives are an explicit lock row (SELECT … FOR UPDATE on a per-subject row) or SERIALIZABLE isolation with retry.