fix: destroy the connection on unlock failure so a still-locked session is never reused (E00-S03-T04)
CI / Frozen lockfile install (pull_request) Successful in 47s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 26s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 25s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 41s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 44s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
CI / Frozen lockfile install (pull_request) Successful in 47s
CI / Secrets not embedded (E00-S02-T08) (pull_request) Successful in 26s
CI / Database-postgres import isolation (E00-S03-T02) (pull_request) Successful in 25s
CI / Migration ledger (E00-S03-T03) (pull_request) Successful in 41s
CI / Migration advisory lock (E00-S03-T04) (pull_request) Successful in 44s
CI / Compose config (E00-S03-T01) (pull_request) Successful in 25s
release() previously returned the connection to the pool in a finally even when the pg_advisory_unlock statement failed, so a pooled connection could be reused while its session still held the migration advisory lock - the next borrower would block every other runner (reviewer finding F4). On unlock failure the connection is now destroyed (client.release(error) removes the client from the pool, ending the session and its lock); the plain client.release() is kept only on the success path. Locked in by a static assertion, a mutation probe, and a deterministic stub-pool behavioral probe of the committed release() control flow.
This commit is contained in:
@@ -21,9 +21,11 @@
|
||||
* (`pool.connect()`), so the lock never taints a pooled connection that
|
||||
* other queries share;
|
||||
* - released explicitly by `release()` (`pg_advisory_unlock` first, then
|
||||
* the client goes back to the pool), and released implicitly by the
|
||||
* server when the holding session ends — the issue's rollback note: "the
|
||||
* lock releases when the runner exits".
|
||||
* the client goes back to the pool; if the unlock statement itself fails,
|
||||
* the connection is destroyed — `client.release(error)` — so a pooled
|
||||
* connection is never reused while its session still holds the lock),
|
||||
* and released implicitly by the server when the holding session ends —
|
||||
* the issue's rollback note: "the lock releases when the runner exits".
|
||||
*
|
||||
* The lock key is always passed as a bound parameter (`$1`) — the SQL never
|
||||
* interpolates it, so the only interpolated value is the `$1` placeholder
|
||||
@@ -133,7 +135,12 @@ export class MigrationLock {
|
||||
* Releases the advisory lock: unlocks it on the holding session
|
||||
* (`pg_advisory_unlock`) and returns the connection to the pool, so the
|
||||
* lock is gone before any other query could reuse that pooled connection.
|
||||
* Idempotent: releasing an instance that does not hold the lock is a no-op.
|
||||
* If the unlock statement fails (e.g. statement timeout or cancellation)
|
||||
* the connection is destroyed instead — `client.release(error)` tells the
|
||||
* pool to drop the client, ending the session (and its lock), so a pooled
|
||||
* connection is never reused while its session still holds the lock; the
|
||||
* failure is re-thrown. Idempotent: releasing an instance that does not
|
||||
* hold the lock is a no-op.
|
||||
*/
|
||||
async release(): Promise<void> {
|
||||
if (!this.held || this.client === null) return;
|
||||
@@ -145,8 +152,15 @@ export class MigrationLock {
|
||||
'SELECT pg_advisory_unlock(hashtextextended($1, 0))',
|
||||
[MIGRATION_LOCK_KEY],
|
||||
);
|
||||
} finally {
|
||||
client.release();
|
||||
} catch (error) {
|
||||
// The unlock statement failed while this session may still hold the
|
||||
// advisory lock. The connection must not go back into the pool in that
|
||||
// state — the next borrower would block every other runner. Destroy it
|
||||
// (release(error) removes the client from the pool), so the session —
|
||||
// and its lock — ends.
|
||||
client.release(error as Error);
|
||||
throw error;
|
||||
}
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user