/** * Per-principal CUMULATIVE cost quota ([ref]/27 follow-up). The per-task `maxCostUsd` gate caps a * SINGLE task; this caps a principal's spend ACROSS tasks within a rolling fixed window. Spend is the * authoritative integer `costMicroUsd` accumulated by the tracer (so it includes council/team * sub-tasks, via the principal ALS context). A new task is REFUSED once the window's accumulated cost * has crossed the ceiling — the in-flight task that crossed it still finishes (the per-task gate is the * single-task guard); the quota blocks the NEXT one until the window rolls over. * * In-memory, per-replica — same trade as the request rate limiter. A multi-replica deployment under- * counts across replicas (each tracks its own slice); a shared (TiDB) quota is the hardening step, * mirroring TiDBBreakerState. Documented, not silently wrong. */ export interface QuotaDecision { allowed: boolean; usedMicroUsd: number; limitMicroUsd: number; /** Seconds until the current window rolls over (only meaningful when !allowed). */ retryAfterSec: number; } /** The hot-path surface shared by the in-memory {@link CostQuota} (single replica) and the cross-replica * {@link import("../plugins/tidb-cost-quota.js").TiDBCostQuota}. `key` is opaque (principal today; * principal/team/feature later — [ref] N3), so both stores are key-based. */ /** [ref] 批1:配额的**值换代**入参(四实现类同一形;单位与构造参数一致 —— USD/秒 → micro/ms 的换算 * 只有一处,`boot/limit-sync.ts` 的 `buildQuotaBounds`)。`limitMicroUsd <= 0` = **关断哨兵**。 */ export interface QuotaLimits { limitMicroUsd: number; windowMs: number; } export interface QuotaTracker { check(key: string): QuotaDecision; add(key: string, micro: number): void; /** [ref]:值换代座位。**接口级**声明——新实现漏掉它 = 编译红,而不是热更新在那条腿上静默失效。 */ setLimits(next: QuotaLimits): void; } export declare class CostQuota implements QuotaTracker { private readonly now; private windows; /** 🔴 不再 `readonly`:限额/窗长是比较参数,换代只动它们,窗内已累计的花费一个不动([ref] §5)。 */ private limitMicroUsd; private windowMs; /** @param limitMicroUsd ceiling per window (integer micro-USD; `<=0` = OFF sentinel). @param windowMs rolling window. */ constructor(limitMicroUsd: number, windowMs: number, now?: () => number); /** * 值换代。两件事**语义不同**,别混: * · **限额**换代 = 纯比较参数 ⇒ 窗内已累计的花费一个不动([ref] §5 定案:调小当场更严)。 * · **窗长**换代 = 换**记账周期** ⇒ 当代累计重开。 * * 🔴 后者是 codex R1-F4(真 finding,红先):SQL 腿的窗长是**桶键的一部分**(`floor(now/windowMs)`), * 换窗长后当代桶天然重算 ⇒ 已累计不跟着搬家;内存腿修前却保留原窗口累计。于是**同一条发布**在 * 内存部署上继续拒绝超额 principal、在 PG/TiDB 部署上重新放行一整个额度 —— 后端相关的成本 fail-open。 * 统一取 SQL 腿那一侧(唯一能在两条腿上都实现的语义:SQL 侧无法把旧桶累计搬进新桶)。 */ setLimits(next: QuotaLimits): void; /** Is this principal under quota right now? (Reads only — does not consume.) */ check(key: string): QuotaDecision; /** Add spend for a principal, starting a fresh window if the prior one expired. */ add(key: string, micro: number): void; /** Drop expired windows (called from the background reaper) to bound memory. */ reap(): number; } //# sourceMappingURL=cost-quota.d.ts.map