import { type ScopeId, type ScopeStorageSample, type StorageGauge, type TenantId } from '@substrat-run/contracts'; import type { RedactionSql } from './subject-redaction.js'; /** * The stored storage gauge (#1524), as statements both adapters run against their directory: * the SQLite host over better-sqlite3, the hosted one inside ControlPlaneDO. Written once so * the two cannot keep different histories of the same fleet. * * One row per (scope, UTC day): the day's LATEST reading, replaced by a later one on the * same day. A day holds one number per scope, which is what a byte-day sum needs and no * more, so the table grows by at most one row per scope per day, and retention * (`STORAGE_GAUGE_RETENTION_MONTHS`) bounds it in time. * * The rows are written by the scheduled pass (`runPlatformSweep`'s storage phase), which * samples only scopes an earlier phase of the same pass already woke. Nothing here reads a * scope; serving a figure is a directory read. * * Beside the samples, `_substrat_scope_storage_attempts` keeps one row per scope: when the * phase last TRIED, and the error if the read failed. It is what the phase picks due scopes * by. Without it a scope whose read keeps failing would stay due and be retried on every * pass, ahead of every scope that can be read, and its error would reach every pass's * failure digest. With it, a failing scope is tried once a day like any other, while its last * good sample stands. */ export declare const SCOPE_STORAGE_DDL = "\n CREATE TABLE IF NOT EXISTS _substrat_scope_storage (\n scope_id TEXT NOT NULL,\n day TEXT NOT NULL,\n tenant_id TEXT NOT NULL,\n bytes INTEGER NOT NULL,\n read_at TEXT NOT NULL,\n PRIMARY KEY (scope_id, day)\n );\n CREATE INDEX IF NOT EXISTS _substrat_scope_storage_tenant ON _substrat_scope_storage (tenant_id, day);\n CREATE INDEX IF NOT EXISTS _substrat_scope_storage_day ON _substrat_scope_storage (day);\n CREATE TABLE IF NOT EXISTS _substrat_scope_storage_attempts (\n scope_id TEXT PRIMARY KEY,\n tenant_id TEXT NOT NULL,\n attempted_at TEXT NOT NULL,\n error TEXT\n );\n CREATE TABLE IF NOT EXISTS _substrat_scope_store_storage (\n scope_id TEXT NOT NULL,\n binding TEXT NOT NULL,\n day TEXT NOT NULL,\n tenant_id TEXT NOT NULL,\n bytes INTEGER NOT NULL,\n read_at TEXT NOT NULL,\n PRIMARY KEY (scope_id, binding, day)\n );\n CREATE INDEX IF NOT EXISTS _substrat_scope_store_storage_tenant ON _substrat_scope_store_storage (tenant_id, day);\n"; /** * The statuses a scope that holds a store can be in: every status but `provisioning` (no * store yet) and `reaped` (its store is gone). A non-serving scope (suspended, archiving, * archived) still holds its storage until reap, so it is measured and belongs on the bill. */ export declare const STORED_SCOPE_STATUSES: readonly ['active', 'suspended', 'archiving', 'archived']; /** * Whether a scope holds a store to measure (#1524): a `STORED_SCOPE_STATUSES` status, unless * it was archived straight from `provisioning` (`archivedFromStatus`). That scope never held * data, so it is neither read (a read would create the spine it never had) nor counted. A row * archived before the column existed has a null `archivedFromStatus` and is measured, as * before. `holdsStoreSql` is the same rule for the directory's SQL. */ export declare function holdsStore(scope: { status: string; archivedFromStatus?: string | null; }): boolean; /** `holdsStore` over a `scopes` row, with an optional table alias (`'s.'`). */ export declare function holdsStoreSql(alias?: string): string; /** How long a day's sample is kept: thirteen months, so a year can be compared with the one before it. */ export declare const STORAGE_GAUGE_RETENTION_MONTHS = 13; /** How many samples one `pruneScopeStorage` call deletes, by default. */ export declare const STORAGE_GAUGE_PRUNE_BATCH = 500; /** The most history rows one `listScopeStorage` call returns, and its default. */ export declare const STORAGE_HISTORY_LIMIT_MAX = 10000; export declare const STORAGE_HISTORY_LIMIT_DEFAULT = 1000; /** What the storage phase hands `recordScopeStorage`: one attempt to read a scope's size. */ export interface ScopeStorageReadingInput { tenantId: TenantId; scopeId: ScopeId; /** The size read, or null when there is none: a failed read (`error` says why), or a scope the reader declined. */ bytes: number | null; error?: string; /** When the read was made. Its UTC date is the sample's day. */ readAt: string; /** * Scope stores read in the same attempt (#2212). Present means this attempt measured * them: an empty list replaces the day's store rows with nothing. Absent leaves them. */ stores?: readonly { binding: string; bytes: number; }[]; } /** When the phase last tried a scope (#1524), and why it failed if it did. */ export interface ScopeStorageAttempt { tenantId: TenantId; scopeId: ScopeId; attemptedAt: string; error: string | null; } /** * Filter for `listScopeStorage`. `latest` returns one row per stored scope, its most * recent day, and ignores `since`/`until`/`limit`: that set is bounded by the scope count, * the same as `listScopes`. Otherwise it is history, ordered (scope, day), `since`/`until` * inclusive UTC days, at most `limit` rows. */ export interface ScopeStorageFilter { tenantId?: TenantId; scopeId?: ScopeId; latest?: boolean; since?: string; until?: string; limit?: number; } /** The UTC day a reading belongs to. */ export declare function storageGaugeDay(readAt: string): string; /** * The first day retention keeps: thirteen calendar months before `nowMs`, as a UTC day. The * day is clamped to the target month's last day, so the 31st of a month counts back to the * 28th, 29th or 30th rather than rolling forward into the month after. */ export declare function storageRetentionHorizon(nowMs: number): string; /** * Record attempts, a statement or two each. Every row is written only for a scope the * directory holds UNDER THAT TENANT and has not reaped, so a reading can never land in another * tenant's figure, and a scope reaped while its read was in flight leaves no row behind. * * Every attempt replaces the scope's attempt row. A successful one also upserts the day's * sample, which replaces an older reading of that day and never a newer one; a failed one * leaves the samples alone, so the last good reading stands. Returns how many SAMPLE rows * were written. */ export declare function recordScopeStorageRows(sql: RedactionSql, readings: readonly ScopeStorageReadingInput[]): number; /** Read samples — see `ScopeStorageFilter`. Every row is parsed on the way out. */ export declare function listScopeStorageRows(sql: RedactionSql, filter?: ScopeStorageFilter): ScopeStorageSample[]; /** * Every stored scope's latest attempt — what the storage phase picks due scopes by, and * what a meter reading names failing scopes from. `tenantId` narrows to one tenant. */ export declare function listScopeStorageAttemptRows(sql: RedactionSql, tenantId?: TenantId): ScopeStorageAttempt[]; /** Delete at most `limit` samples older than retention, oldest first, across both tables. Returns how many went. */ export declare function pruneScopeStorageRows(sql: RedactionSql, nowMs: number, limit: number): number; /** * Bindings on the latest day that measured stores for the scope. A binding absent that day has * stopped, and a day that measured none leaves only its marker, which is not a binding. */ export declare function listScopeStoreStorageRows(sql: RedactionSql, tenantId?: TenantId): { tenantId: TenantId; bytes: number; readAt: string; }[]; /** Drop a scope's samples and attempt: its storage is gone, so its history describes nothing billable. */ export declare function forgetScopeStorage(sql: RedactionSql, scopeId: string): void; /** * Fold latest samples into one gauge. `total` is the caller's count of stored scopes * (`STORED_SCOPE_STATUSES`); samples are expected to be the latest per stored scope * (`listScopeStorage({ latest })`), and attempts the latest per stored scope * (`listScopeStorageAttempts`). */ export declare function foldStorageGauge(samples: readonly { bytes: number; readAt: string; }[], total: number, attempts?: readonly { attemptedAt: string; error: string | null; }[], stores?: readonly { bytes: number; }[]): StorageGauge; //# sourceMappingURL=storage-gauge.d.ts.map