Initial Commit
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
# `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.
|
||||
Reference in New Issue
Block a user