/** * Cross-replica circuit-breaker state (core 1.38 `BreakerState`) — SINGLE-FILE DUAL-DIALECT * ([ref] A12 定型半场). ONE implementation, TWO dialects; the historical `TiDBBreakerState` / * `PgBreakerState` class names survive as thin ctor subclasses in THIS file ([ref]: the old per-dialect shim files are deleted) so every consumer (store-backend.ts, the * fake-pool unit suites, the real dual-DB integration suite) is untouched. * * core's `createCircuitBreakerBrain` defaults to a per-PROCESS Map → on a multi-replica deployment * every replica re-discovers a provider outage independently (slow fleet-wide reaction). This backs * the breaker with the shared `circuit_breaker` table so a trip on one replica is learned by the others. * * IMPORTANT — `BreakerState.get/set` are **synchronous** (core calls them on the hot path of every * brain invocation; a DB round-trip per call is untenable). So this is a **write-through cache**: * - `get` / `set` operate on an in-process Map synchronously (so the local breaker is always correct); * - `set` ALSO fires an async write-through to the shared table (best-effort; a DB hiccup must never * break the request path — breaker state is advisory); * - a periodic `refresh()` pulls the shared table back into the cache so this replica learns peers' * trips. Consistency is therefore **eventual** (a replica may send a few extra requests before it * learns a peer already opened the breaker — self-healing, never wrong in a dangerous direction). * * Strict per-call linearizable state would need an async `BreakerState` from core (a separate effort); * for a consultative breaker, eventually-consistent write-through is the right trade (see core [ref]). * * ── Dialect deltas, kept EXPLICIT (the ONLY place the two engines actually differ) ───────────────── * - placeholders `?` vs `$n` * - upsert form: `ON DUPLICATE KEY UPDATE … VALUES(col)` vs `ON CONFLICT (breaker_key) DO UPDATE SET * … EXCLUDED.col` * - schema ownership: TiDB DDL lives centrally in tidb-pool.ts; PG DDL is self-contained here * (`PG_BREAKER_STATE_SCHEMA` / `ensureSchema`, exported from THIS file) — disjoint table, * so a standalone idempotent apply is safe (no central-schema shadowing), mirroring every other PG store. * Everything else (write-through logic, the pending-guard race fix, refresh/prune, row→snapshot mapping, * opened_at Date handling) is IDENTICAL — the SqlDriver seam (result unwrapping) is what let the row * mapping collapse to one body; both drivers hand back the same `SqlRow` shape. * * Kept in lock-step with the real engines by test/pg-breaker-state-integration.test.ts (runs the SAME * scenarios on BOTH real engines) and the cross-instance concurrency pin in * test/file-stores-cross-instance-concurrency.test.ts (last-writer-wins overwrite semantics — a design * question, not a defect; see that file's comment for the full reasoning). */ import type { Pool as MySqlPool } from "mysql2/promise"; import type { Pool as PgPool, PoolClient } from "pg"; import type { BreakerSnapshot, BreakerState } from "@sema-agent/core"; import { type SqlDriver } from "./sql-driver.js"; /** PG translation of the tidb-pool.ts `circuit_breaker` DDL (DATETIME(3)→TIMESTAMPTZ(3)). Disjoint from other * stores' tables, so a self-contained ensureSchema is safe (no central-schema shadowing). TiDB's DDL lives * centrally in tidb-pool.ts (no PG-side twin needed there — production wires ONE ensureTidbSchema). */ export declare const PG_BREAKER_STATE_SCHEMA: string[]; /** Idempotent schema apply for the breaker table (the PG twin of tidb-pool.ts ensureSchema, scoped to this store). */ export declare function ensureSchema(pool: PgPool | PoolClient): Promise; /** Dual-dialect cross-replica BreakerState. See the file header for the dialect-delta ledger. */ export declare class SqlBreakerState implements BreakerState { protected readonly db: SqlDriver; /** LOW (SILENT-FALLBACK P1): write-through failure tap — streak per call, 0 on the recovery edge. */ private readonly onWriteFail?; private cache; private timer?; /** Keys with an in-flight write-through upsert. A concurrent `refresh()` must NOT overwrite or prune * these — their newer local value may not be in the DB yet. Closes the set()-vs-refresh race (core * 1.40.0 council finding #5). */ private readonly pending; private writeFailStreak; constructor(db: SqlDriver, /** LOW (SILENT-FALLBACK P1): write-through failure tap — streak per call, 0 on the recovery edge. */ onWriteFail?: ((streak: number) => void) | undefined); /** Pick the dialect's SQL text. Both statements stay written out at the call site ON PURPOSE. */ private q; get(key: string): BreakerSnapshot | undefined; set(key: string, snap: BreakerSnapshot): void; private writeThrough; /** * Pull the shared table into the local cache so this replica learns peers' trips, and PRUNE keys that * vanished from the DB — but never touch a locally-pending write (its upsert may not have landed yet), * else a just-`set` trip would be clobbered/reverted before it reaches the DB. Eventually consistent: * a replica is at most one refresh interval behind a peer, never wrong in a data-losing way. * `halfOpenInFlight` is intentionally not shared (it is a per-replica probe counter). * * SELECT text is byte-identical on both dialects (no placeholders) — written once, not through `q()` * (same precedent as image-bake-store-sql.ts's queued-bake lookup). */ refresh(): Promise; /** Start the periodic refresh loop. `unref` so it never holds the process open. Returns `this`. */ startRefresh(intervalMs?: number): this; stop(): void; } /** MySQL-protocol (TiDB) binding — historical class name + ctor shape preserved. */ export declare class TiDBBreakerState extends SqlBreakerState { constructor(pool: MySqlPool, onWriteFail?: (streak: number) => void); } /** PostgreSQL binding — historical class name + ctor shape preserved. */ export declare class PgBreakerState extends SqlBreakerState { constructor(pool: PgPool, onWriteFail?: (streak: number) => void); } //# sourceMappingURL=breaker-state-sql.d.ts.map