@@ -14,6 +14,17 @@ export type AdvisoryLockId = (typeof ADVISORY_LOCK_IDS)[keyof typeof ADVISORY_LO
1414
1515const HEALTH_CHECK_INTERVAL_MS = 10_000 // 10 seconds
1616
17+ /**
18+ * Coerces a postgres boolean result to a native boolean.
19+ * postgres can return 't'/'f' strings when type parsing is disabled,
20+ * or actual boolean values depending on configuration.
21+ */
22+ function coerceBool ( value : unknown ) : boolean {
23+ if ( typeof value === 'boolean' ) return value
24+ if ( value === 't' || value === 'true' || value === 1 ) return true
25+ return false
26+ }
27+
1728// Diagnostic logging helper with timestamp and process info
1829function logLock ( level : 'info' | 'error' | 'warn' , message : string , data ?: Record < string , unknown > ) : void {
1930 const timestamp = new Date ( ) . toISOString ( )
@@ -54,12 +65,13 @@ export async function tryAcquireAdvisoryLock(lockId: AdvisoryLockId): Promise<{
5465 max : 1 ,
5566 idle_timeout : 0 ,
5667 connect_timeout : 10 ,
68+ max_lifetime : 0 , // Disable connection recycling - must keep session alive for advisory lock
5769 } )
5870
5971 try {
6072 logLock ( 'info' , 'Database connection established, attempting pg_try_advisory_lock' )
6173 const result = await connection `SELECT pg_try_advisory_lock(${ lockId } ) as acquired`
62- const acquired = result [ 0 ] ?. acquired === true
74+ const acquired = coerceBool ( result [ 0 ] ?. acquired )
6375
6476 logLock ( 'info' , 'Lock acquisition result' , { acquired, lockId } )
6577
@@ -74,11 +86,14 @@ export async function tryAcquireAdvisoryLock(lockId: AdvisoryLockId): Promise<{
7486 // Create the lock handle
7587 let lostCallback : ( ( ) => void ) | null = null
7688 let isReleased = false
89+ let lostTriggered = false // Track if lost was triggered before callback registered
7790 let healthCheckTimer : ReturnType < typeof setInterval > | null = null
7891 let healthCheckCount = 0
92+ let healthCheckInFlight = false // Guard against stacking health checks
7993
8094 const triggerLost = ( ) => {
81- if ( isReleased ) return
95+ if ( isReleased || lostTriggered ) return
96+ lostTriggered = true
8297 logLock ( 'warn' , 'Lock lost detected, triggering lost callback' , { lockId, healthCheckCount } )
8398 if ( healthCheckTimer ) {
8499 clearInterval ( healthCheckTimer )
@@ -94,7 +109,8 @@ export async function tryAcquireAdvisoryLock(lockId: AdvisoryLockId): Promise<{
94109
95110 // Start health check interval - verify we still hold the lock, not just connection liveness
96111 healthCheckTimer = setInterval ( async ( ) => {
97- if ( isReleased ) return
112+ if ( isReleased || healthCheckInFlight ) return
113+ healthCheckInFlight = true
98114 healthCheckCount ++
99115 try {
100116 // Query pg_locks to verify we still hold this specific advisory lock
@@ -109,7 +125,7 @@ export async function tryAcquireAdvisoryLock(lockId: AdvisoryLockId): Promise<{
109125 AND granted = true
110126 ) as held
111127 `
112- const stillHeld = result [ 0 ] ?. held === true
128+ const stillHeld = coerceBool ( result [ 0 ] ?. held )
113129 if ( ! stillHeld ) {
114130 logLock ( 'error' , 'Advisory lock health check failed - lock no longer held' , { lockId, healthCheckCount } )
115131 triggerLost ( )
@@ -120,12 +136,18 @@ export async function tryAcquireAdvisoryLock(lockId: AdvisoryLockId): Promise<{
120136 } catch ( error ) {
121137 logLock ( 'error' , 'Advisory lock health check failed - connection lost' , { lockId, healthCheckCount, error : String ( error ) } )
122138 triggerLost ( )
139+ } finally {
140+ healthCheckInFlight = false
123141 }
124142 } , HEALTH_CHECK_INTERVAL_MS )
125143
126144 const handle : LockHandle = {
127145 onLost ( callback : ( ) => void ) {
128146 lostCallback = callback
147+ // If lost was already triggered before callback was registered, invoke immediately
148+ if ( lostTriggered ) {
149+ callback ( )
150+ }
129151 } ,
130152 async release ( ) {
131153 if ( isReleased ) {
0 commit comments