Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,2 @@
export { OutstandingPreparedCap } from "./outstanding-cap.js";
export { assertTimeoutBelowCeiling } from "./timeout-ceiling.js";

View File

@@ -0,0 +1,104 @@
# `limits/outstanding-cap`
## Purpose
Admission control for in-flight 2PC transactions. The coordinator
holds a fixed-size budget of "outstanding prepared transactions"; once
the budget is exhausted new `run2pc` calls refuse rather than queue.
The default is 256 (design doc §14's open question, pinned by this
package's spec) and operators can tune it via host config.
Refusing rather than queuing is intentional: Postgres holds row-level
locks for every prepared transaction, so a backed-up coordinator
would otherwise convert a transient overload into a global lock
storm. Failing fast lets callers route to a degraded path
(degraded-write, retry queue, etc.).
## Features
- `OutstandingPreparedCap` — synchronous counter with `admit()`,
`release()`, `snapshot()`, `capacity()`.
- Constructor validates that the cap is a positive integer (rejects
zero, negatives, fractions, `NaN`).
- `release()` is idempotent below zero — a buggy double-release in
caller code cannot silently expand the real cap.
- Synchronous-only — relies on the event-loop atomicity guarantee
between `admit` / `release` pairs in JavaScript runtimes.
## Usage: Basic
```ts
import { OutstandingPreparedCap } from "@suaveplan/api-2pc/limits";
const cap = new OutstandingPreparedCap(256);
cap.admit();
try {
// run the 2PC transaction
} finally {
cap.release();
}
```
## Usage: Advanced
Wrap admission inside the coordinator's public entry to centralise the
release in one `finally`. The snapshot fuels the OTel gauge:
```ts
import { TWOPC_CODES, TwoPcError } from "@suaveplan/api-2pc/error-codes";
import { OutstandingPreparedCap } from "@suaveplan/api-2pc/limits";
class MiniCoordinator {
private readonly cap = new OutstandingPreparedCap(256);
snapshot() {
return this.cap.snapshot();
}
async run<T>(fn: () => Promise<T>): Promise<T> {
try {
this.cap.admit();
} catch (err) {
if (
err instanceof TwoPcError &&
err.twoPcCode === TWOPC_CODES.OUTSTANDING_LIMIT_EXCEEDED
) {
// route to degraded path
throw err;
}
throw err;
}
try {
return await fn();
} finally {
this.cap.release();
}
}
}
```
## API Reference
- `class OutstandingPreparedCap`
- `constructor(cap: number)` — throws
`TWOPC_OUTSTANDING_LIMIT_EXCEEDED` on invalid `cap`.
- `admit(): void` — throws
`TWOPC_OUTSTANDING_LIMIT_EXCEEDED` when at capacity.
- `release(): void` — idempotent below zero.
- `snapshot(): number` — current outstanding count.
- `capacity(): number` — configured cap.
## Implementation Notes
Construction-time validation re-uses the same error code as the
runtime cap-exceeded path. This is deliberate: operators tuning the
cap via host config want one error string to alert on, regardless of
whether the misconfiguration was a fractional value at startup or a
real overload at steady state. The context object on the thrown
`TwoPcError` distinguishes the two cases (`cap` only vs.
`cap` + `outstanding`).
The class is intentionally tiny — no async, no internal queue, no
external dependencies. Adding a queue here would re-introduce the
lock-storm risk the cap exists to prevent.

View File

@@ -0,0 +1,58 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { TWOPC_CODES, TwoPcError } from "../error-codes/error-codes.js";
import { OutstandingPreparedCap } from "./outstanding-cap.js";
describe("OutstandingPreparedCap", () => {
it("admits up to the configured cap", { timeout: 5000 }, () => {
const cap = new OutstandingPreparedCap(3);
cap.admit();
cap.admit();
cap.admit();
expect(cap.snapshot()).toBe(3);
expect(cap.capacity()).toBe(3);
});
it("rejects the (cap+1)th admission with TWOPC.OUTSTANDING_LIMIT_EXCEEDED", {
timeout: 5000,
}, () => {
const cap = new OutstandingPreparedCap(2);
cap.admit();
cap.admit();
try {
cap.admit();
throw new Error("should throw");
} catch (err) {
expect(err).toBeInstanceOf(TwoPcError);
expect((err as TwoPcError).twoPcCode).toBe(
TWOPC_CODES.OUTSTANDING_LIMIT_EXCEEDED
);
}
});
it("recovers as transactions release slots", { timeout: 5000 }, () => {
const cap = new OutstandingPreparedCap(1);
cap.admit();
expect(() => cap.admit()).toThrow();
cap.release();
cap.admit();
expect(cap.snapshot()).toBe(1);
});
it("release below zero clamps at zero (defends against double-release)", {
timeout: 5000,
}, () => {
const cap = new OutstandingPreparedCap(2);
cap.release();
cap.release();
expect(cap.snapshot()).toBe(0);
});
it.each([
0,
-1,
1.5,
Number.NaN,
])("constructor rejects non-positive-integer cap %p", (bad) => {
expect(() => new OutstandingPreparedCap(bad as number)).toThrow();
});
});

View File

@@ -0,0 +1,64 @@
import { TWOPC_CODES, TwoPcError } from "../error-codes/error-codes.js";
/**
* Admission counter for outstanding prepared transactions. Tracks how
* many `run2pc` calls are between `admit()` and `release()`; new calls
* past the cap raise `TWOPC_OUTSTANDING_LIMIT_EXCEEDED` until an
* in-flight transaction releases a slot.
*
* The implementation is single-threaded — `admit` and `release` are
* synchronous balanced calls. Bun's event loop guarantees the call
* pair is atomic relative to other tasks, so no mutex is required.
*/
export class OutstandingPreparedCap {
private outstanding = 0;
public constructor(private readonly cap: number) {
if (!Number.isInteger(cap) || cap <= 0) {
throw new TwoPcError(
TWOPC_CODES.OUTSTANDING_LIMIT_EXCEEDED,
`outstandingPreparedCap must be a positive integer; got ${cap}`,
{ context: { cap } }
);
}
}
/**
* Reserve a slot. Throws
* `TWOPC_OUTSTANDING_LIMIT_EXCEEDED` when the cap is already at
* capacity. On success the caller MUST eventually invoke
* `release()` in a `finally` block.
*/
public admit(): void {
if (this.outstanding >= this.cap) {
throw new TwoPcError(
TWOPC_CODES.OUTSTANDING_LIMIT_EXCEEDED,
`Outstanding prepared transactions at cap ${this.cap}`,
{ context: { cap: this.cap, outstanding: this.outstanding } }
);
}
this.outstanding += 1;
}
/**
* Release the slot reserved by a matching {@link admit}. Idempotent
* against arithmetic underflow — repeated calls clamp at zero so
* a buggy double-release in caller code does not allow the next
* `admit` to silently exceed the real cap.
*/
public release(): void {
if (this.outstanding > 0) {
this.outstanding -= 1;
}
}
/** Current outstanding count. Snapshot read for the OTel gauge. */
public snapshot(): number {
return this.outstanding;
}
/** Configured cap. Diagnostics only. */
public capacity(): number {
return this.cap;
}
}

View File

@@ -0,0 +1,77 @@
# `limits/timeout-ceiling`
## Purpose
Enforces the hard 30 000 ms ceiling on caller-supplied `timeoutMs`
values. The check fires at the public entry point of `run2pc` so
above-ceiling calls never contact a participant; the operational
warning in design doc §9.2 is that long prepares hold row-level locks,
so surfacing the misuse loudly is more useful than silently clamping.
This module is intentionally tiny — one function — because the
behaviour is small but the gate is one of the most-asserted contracts
in the spec.
## Features
- `assertTimeoutBelowCeiling(timeoutMs)` — throws
`TWOPC_TIMEOUT_CEILING_EXCEEDED` for above-ceiling, non-positive,
non-finite, or `NaN` inputs.
- Re-uses the `TIMEOUT_CEILING_MS` constant from `options/` so the
ceiling stays a single source of truth.
## Usage: Basic
```ts
import { assertTimeoutBelowCeiling } from "@suaveplan/api-2pc/limits";
assertTimeoutBelowCeiling(5000); // ok
assertTimeoutBelowCeiling(30001); // throws TWOPC_TIMEOUT_CEILING_EXCEEDED
```
## Usage: Advanced
The function is intentionally side-effect-free so callers can wrap it
in a per-tenant override path:
```ts
import { TIMEOUT_CEILING_MS } from "@suaveplan/api-2pc/options";
import { assertTimeoutBelowCeiling } from "@suaveplan/api-2pc/limits";
function resolveTimeout(
requested: number | undefined,
tenantMax: number
): number {
const effective = requested ?? DEFAULT_TIMEOUT_MS;
assertTimeoutBelowCeiling(effective);
if (effective > tenantMax) {
return tenantMax;
}
if (effective > TIMEOUT_CEILING_MS) {
// unreachable given the assert above — included only to
// illustrate that the ceiling acts as the strict upper bound.
return TIMEOUT_CEILING_MS;
}
return effective;
}
```
## API Reference
- `function assertTimeoutBelowCeiling(timeoutMs: number): void`
- Throws `TwoPcError(TWOPC_CODES.TIMEOUT_CEILING_EXCEEDED)` for any
of: `NaN`, `Infinity`, `-Infinity`, values `<= 0`, values
`> TIMEOUT_CEILING_MS`.
## Implementation Notes
The check rejects non-positive values for the same reason it rejects
above-ceiling ones: an immediate or backwards-in-time deadline would
collapse phase 1 to a single tick and the rollback path would fire
before any participant could finish a real prepare. Treating that as
the same misuse class as "30 minutes" is a deliberate ergonomic
choice — operators get one error code to grep for.
`Number.isFinite` is used in preference to `Number.isNaN` because the
former covers `Infinity` and `-Infinity` in addition to `NaN`. The
explicit `<= 0` check then excludes zero and negative-finite values.

View File

@@ -0,0 +1,54 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import { TWOPC_CODES, TwoPcError } from "../error-codes/error-codes.js";
import { DEFAULT_TIMEOUT_MS, TIMEOUT_CEILING_MS } from "../options/options.js";
import { assertTimeoutBelowCeiling } from "./timeout-ceiling.js";
function expectCeilingError(fn: () => void): TwoPcError {
try {
fn();
} catch (err) {
expect(err).toBeInstanceOf(TwoPcError);
const e = err as TwoPcError;
expect(e.twoPcCode).toBe(TWOPC_CODES.TIMEOUT_CEILING_EXCEEDED);
return e;
}
throw new Error("expected TWOPC_TIMEOUT_CEILING_EXCEEDED");
}
describe("assertTimeoutBelowCeiling", () => {
it("accepts the default timeoutMs (5000)", { timeout: 5000 }, () => {
expect(() =>
assertTimeoutBelowCeiling(DEFAULT_TIMEOUT_MS)
).not.toThrow();
});
it("accepts a value equal to the ceiling", { timeout: 5000 }, () => {
expect(() =>
assertTimeoutBelowCeiling(TIMEOUT_CEILING_MS)
).not.toThrow();
});
it("rejects timeoutMs above the ceiling", { timeout: 5000 }, () => {
expectCeilingError(() =>
assertTimeoutBelowCeiling(TIMEOUT_CEILING_MS + 1)
);
});
it("rejects zero", { timeout: 5000 }, () => {
expectCeilingError(() => assertTimeoutBelowCeiling(0));
});
it("rejects negative values", { timeout: 5000 }, () => {
expectCeilingError(() => assertTimeoutBelowCeiling(-1));
});
it("rejects NaN", { timeout: 5000 }, () => {
expectCeilingError(() => assertTimeoutBelowCeiling(Number.NaN));
});
it("rejects Infinity", { timeout: 5000 }, () => {
expectCeilingError(() =>
assertTimeoutBelowCeiling(Number.POSITIVE_INFINITY)
);
});
});

View File

@@ -0,0 +1,29 @@
import { TWOPC_CODES, TwoPcError } from "../error-codes/error-codes.js";
import { TIMEOUT_CEILING_MS } from "../options/options.js";
/**
* Validate a caller-supplied `timeoutMs`. Per spec the coordinator
* MUST refuse values above the hard ceiling (default 30 000 ms) at
* call time — silently clamping would hide the misuse and let
* long-held prepares regress production throughput.
*
* Negative or non-finite values are also refused — they would
* collapse the deadline to "immediate" which is operationally
* indistinguishable from a flaky participant.
*/
export function assertTimeoutBelowCeiling(timeoutMs: number): void {
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) {
throw new TwoPcError(
TWOPC_CODES.TIMEOUT_CEILING_EXCEEDED,
`timeoutMs must be a positive finite number; got ${timeoutMs}`,
{ context: { timeoutMs } }
);
}
if (timeoutMs > TIMEOUT_CEILING_MS) {
throw new TwoPcError(
TWOPC_CODES.TIMEOUT_CEILING_EXCEEDED,
`timeoutMs ${timeoutMs} exceeds hard ceiling ${TIMEOUT_CEILING_MS}`,
{ context: { timeoutMs, ceiling: TIMEOUT_CEILING_MS } }
);
}
}