109 lines
4.2 KiB
Markdown
109 lines
4.2 KiB
Markdown
# `budget` — `BudgetTracker`
|
|
|
|
<!-- begin:auto-docs -->
|
|
_(no submodules)_
|
|
<!-- end:auto-docs -->
|
|
|
|
## Purpose
|
|
|
|
This module owns the per-request resource-budget enforcement for the
|
|
sandboxed tier. Sampling primitives (CPU percentage, resident memory,
|
|
wall-clock progress) are supplied by the worker host because they live
|
|
outside this package's dependency budget; this module aggregates
|
|
samples against the policy's `budgets` block and raises
|
|
`SandboxPermissionError(code: SAND_PERM.BUDGET_EXCEEDED)` on the first
|
|
newly-breached dimension.
|
|
|
|
The tracker is idempotent across repeat samples on the SAME dimension:
|
|
once a dimension has breached, subsequent ticks against that dimension
|
|
do NOT re-emit the deny counter or re-throw — the dimension is
|
|
considered "tripped" until the tracker is discarded at request end.
|
|
|
|
## Features
|
|
|
|
- `BudgetTracker.tick(sample)` — consumes one `{ cpuPercent, memoryMb }`
|
|
sample plus the wall-clock from the injected `Clock`.
|
|
- `BudgetTracker.elapsedMs()` — milliseconds elapsed since construction.
|
|
- Idempotent breaches — `cpuPercent`, `memoryMb`, `wallClockMs` each
|
|
emit at most once per tracker instance.
|
|
- Throws `SandboxPermissionError` with `code === SAND_PERM.BUDGET_EXCEEDED`
|
|
on the first newly-breached dimension and increments
|
|
`plugin.budget_exceeded_total{plugin, budget}`.
|
|
|
|
## Usage: Basic
|
|
|
|
```ts
|
|
import { BudgetTracker } from "@suaveplan/api-sandbox-permissions/budget";
|
|
import { createPermissionInstruments } from "@suaveplan/api-sandbox-permissions/instruments";
|
|
import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy";
|
|
|
|
const policy = new PermissionPolicy({
|
|
permissions: { budgets: { cpuPercent: 50, memoryMb: 256, wallClockMs: 30_000 } },
|
|
});
|
|
const instruments = createPermissionInstruments({ plugin: "billing" });
|
|
const clock = { now: () => Date.now() };
|
|
const tracker = new BudgetTracker({ policy, instruments, clock });
|
|
|
|
tracker.tick({ cpuPercent: 10, memoryMb: 64 }); // within budget — no throw
|
|
```
|
|
|
|
## Usage: Advanced
|
|
|
|
```ts
|
|
import { BudgetTracker } from "@suaveplan/api-sandbox-permissions/budget";
|
|
import { createPermissionInstruments } from "@suaveplan/api-sandbox-permissions/instruments";
|
|
import { PermissionPolicy } from "@suaveplan/api-sandbox-permissions/policy";
|
|
import { SAND_PERM_CODES, SandboxPermissionError } from "@suaveplan/api-sandbox-permissions/error-codes";
|
|
|
|
const policy = new PermissionPolicy({
|
|
permissions: { budgets: { wallClockMs: 30_000 } },
|
|
});
|
|
const instruments = createPermissionInstruments({ plugin: "billing" });
|
|
|
|
// Injectable Clock — drive the wall-clock breach deterministically.
|
|
let t = 0;
|
|
const clock = { now: () => t };
|
|
const tracker = new BudgetTracker({ policy, instruments, clock });
|
|
|
|
t = 30_001;
|
|
try {
|
|
tracker.tick({ cpuPercent: 0, memoryMb: 0 });
|
|
} catch (err) {
|
|
if (err instanceof SandboxPermissionError) {
|
|
console.assert(err.code === SAND_PERM_CODES.BUDGET_EXCEEDED);
|
|
// err.context = { capability: "budget", reason: "wallClockMs" }
|
|
}
|
|
}
|
|
|
|
// Subsequent ticks against the same dimension are no-ops — the
|
|
// dimension is already tripped.
|
|
t = 35_000;
|
|
tracker.tick({ cpuPercent: 0, memoryMb: 0 }); // returns silently
|
|
```
|
|
|
|
## API Reference
|
|
|
|
- `interface BudgetSample { cpuPercent: number; memoryMb: number; }`
|
|
- `interface BudgetTrackerOptions { policy; instruments; clock: Clock; }`
|
|
- `type BudgetBreach = "cpuPercent" | "memoryMb" | "wallClockMs"`
|
|
- `class BudgetTracker`
|
|
- `constructor(options)`
|
|
- `tick(sample): void` — throws on first newly-breached dimension.
|
|
- `elapsedMs(): number`
|
|
- `readonly policy: PermissionPolicy`
|
|
- `readonly instruments: PermissionInstruments`
|
|
|
|
## Implementation Notes
|
|
|
|
The `Clock` is injected per `@suaveplan/types` — never pulled from
|
|
`Date.now()` directly so test code can drive a deterministic timeline
|
|
without `vi.useFakeTimers()`. The constructor records `startedAt = clock.now()`
|
|
once; every subsequent `elapsedMs()` call subtracts from a fresh
|
|
`clock.now()` so wall-clock progress is monotonic with the clock.
|
|
|
|
`evaluate` is private and centralises the breach record so all three
|
|
dimensions go through the SAME counter + throw codepath; this guarantees
|
|
the conformance harness's "deny counter increments exactly once per
|
|
breach" invariant holds for every dimension without per-dimension
|
|
duplication.
|