Retry & Pooling
Retry behaviour for read-only statements, pool statistics and health checks
Retry configuration
export interface DBConfig { type: DBType; connectionString: string; retryAttempts?: number; // default 3 retryDelay?: number; // default 1000 (ms) maxJitter?: number; // default 100 (ms)}Fields:
- retryAttempts Number of attempts for read-only statements. Default
3. Set to1to effectively disable retry. - retryDelay Base delay in milliseconds. Default
1000. - maxJitter Upper bound of the random jitter added to each backoff, in milliseconds. Default
100.
const orm = new Stabilize({ type: DBType.Postgres, connectionString: process.env.DATABASE_URL!, retryAttempts: 5, retryDelay: 500, maxJitter: 100,});Backoff formula
The delay before each attempt is exponential with a random jitter added:
retryDelay * Math.pow(2, attempt - 1) + getJitter()
// wheregetJitter = () => Math.random() * this.maxJitterThe multiplier is always 2 and is not configurable. There are no initialDelay or maxDelay options, and no configurable list of retryable error codes.
Retry applies to reads only
const attempts = isReadOnlyStatement(query) ? this.retryAttempts : 1;A statement is treated as read-only when, after leading whitespace and comments are stripped, it begins with one of these prefixes:
["SELECT", "PRAGMA", "SHOW", "EXPLAIN", "VALUES"]Every write statement runs exactly once, always — there is no retry of write statements.
No connection pool configuration
There is no pool configuration. There are no max, min, idleTimeout or connectionLimit fields. Every driver object is built from connectionString alone:
// MySQLthis.client = mysql.createPool(config.connectionString);
// Postgresthis.client = new Pool({ connectionString: config.connectionString! });
// SQL Serverthis.client = new sql.ConnectionPool(config.connectionString);
// SQLite — a file handle, not a poolthis.client = new Database(config.connectionString, { create: true });The SQL Server pool is built but left unconnected: connect() is async and the constructor is not, so the pool is opened lazily on the first statement instead.
poolStats()
async poolStats(): Promise<{ active: number; idle: number; total: number }>Returns pool counters. The shape is the same for every driver, but the values are produced differently:
// Postgres — active is hardcoded 0, not measured{ active: 0, idle: 0, total: raw.totalCount }
// MySQL{ active: raw._allConnections.length, idle: raw._freeConnections?.length ?? 0, total: raw._allConnections.length,}
// SQL Server — read through the DBClient wrapper one level down{ active: pool.borrowed ?? 0, idle: pool.available ?? 0, total: pool.size ?? 0,}
// SQLite — no pool; sentinels meaning "not applicable", not an error{ active: -1, idle: -1, total: -1 }const stats = await orm.poolStats();console.log(stats.active, stats.idle, stats.total);healthCheck() — Stabilize
async healthCheck(): Promise<{ status: string; database: string; latencyMs: number; cacheStatus: string }>status is "healthy" or "unhealthy". database is the DBType. cacheStatus names the backend rather than reducing it to connected-or-not: "disabled" when enabled was false, "in-memory" when there is no redisUrl and the cache is confined to this process, "connected" or "connected (miss)" for a Redis client that did or did not find its probe key, and "unknown" when the check threw.
const health = await orm.healthCheck();// { status: "healthy", database: "sqlite", latencyMs: 0.5, cacheStatus: "disabled" }healthCheck() — Repository
async healthCheck(): Promise<{ status: string; table: string; rows: number; latencyMs: number }>The repository variant reports on a single table. rows is -1 when the row count failed.
const health = await userRepo.healthCheck();// { status: "healthy", table: "users", rows: 128, latencyMs: 0.4 }