fix: memoize in-flight acquire so concurrent acquire() is re-entrant-safe (E00-S03-T04)
CI / Frozen lockfile install (pull_request) Successful in 49s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 26s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 26s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 45s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 50s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
CI / Frozen lockfile install (pull_request) Successful in 49s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 26s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 26s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 45s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 50s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
Security-review finding F2: two concurrent acquire() calls on the same MigrationLock instance could each check out a connection; the second pg_advisory_lock would overwrite this.client, leaking the first locked connection until session end. acquire() now memoizes the in-flight acquire in acquireInFlight and returns it on re-entry, so exactly one connection is checked out and no locked connection leaks. The memo is cleared once the acquire settles. tryAcquire()/release() paths unchanged. Locked in by: - static criterion test: acquireInFlight field, re-entry guard returns the in-flight acquire, memo cleared on settle - mutation probe: removing the re-entry guard fails the criterion - real-stack probe: two concurrent acquire() calls on one instance leave pool.totalCount at 1 (exactly one connection), the lock granted once, nothing left after release; probe fails (hangs) on the pre-fix code - CI job comment updated to reflect the re-entrancy criterion
This commit is contained in:
@@ -47,6 +47,8 @@ export class MigrationLock {
|
||||
private readonly pool: Pool;
|
||||
private client: PoolClient | null = null;
|
||||
private held = false;
|
||||
/** In-flight acquire, memoized so concurrent acquire() calls share one connection (re-entrant-safe). */
|
||||
private acquireInFlight: Promise<void> | null = null;
|
||||
|
||||
/** @param pool The package-owned PostgreSQL pool (`pg.Pool`). */
|
||||
constructor(pool: Pool) {
|
||||
@@ -61,12 +63,30 @@ export class MigrationLock {
|
||||
/**
|
||||
* Acquires the migration advisory lock, blocking until it is free — a
|
||||
* second runner waits here while the first holds the lock. Idempotent:
|
||||
* acquiring an already-held instance is a no-op. The lock is held on a
|
||||
* dedicated connection until `release()` (or until the session ends — e.g.
|
||||
* the runner exits and the pool closes its connections).
|
||||
* acquiring an already-held instance is a no-op. Re-entrant-safe:
|
||||
* concurrent `acquire()` calls on the same instance share the single
|
||||
* in-flight acquire (memoized in `acquireInFlight`), so exactly one
|
||||
* connection is checked out and no locked connection leaks. The lock is
|
||||
* held on a dedicated connection until `release()` (or until the session
|
||||
* ends — e.g. the runner exits and the pool closes its connections).
|
||||
*/
|
||||
async acquire(): Promise<void> {
|
||||
if (this.held) return;
|
||||
// A second concurrent acquire() on this instance returns the in-flight
|
||||
// acquire instead of checking out another connection: exactly one
|
||||
// connection is checked out and no locked connection leaks.
|
||||
if (this.acquireInFlight !== null) return this.acquireInFlight;
|
||||
const inFlight = this.doAcquire();
|
||||
this.acquireInFlight = inFlight;
|
||||
try {
|
||||
await inFlight;
|
||||
} finally {
|
||||
this.acquireInFlight = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** The memoized acquire body: checks out one dedicated connection and takes the session-scoped lock on it. */
|
||||
private async doAcquire(): Promise<void> {
|
||||
const client = await this.pool.connect();
|
||||
try {
|
||||
await client.query(
|
||||
|
||||
Reference in New Issue
Block a user