Skip to content

Commit e9cca21

Browse files
committed
fix(internal): Fix advisory lock bugs causing Discord bot to fail to start
- Add coerceBool() helper to handle postgres returning t/f strings instead of booleans - Add max_lifetime: 0 to prevent connection recycling that drops the lock - Add healthCheckInFlight guard to prevent stacking health checks - Add lostTriggered flag to handle race condition with onLost callback The strict === true comparison failed when postgres type parsing returned string values, causing lock acquisition to always report as failed.
1 parent 3acf0d4 commit e9cca21

File tree

1 file changed

+26
-4
lines changed

1 file changed

+26
-4
lines changed

packages/internal/src/db/advisory-lock.ts

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ export type AdvisoryLockId = (typeof ADVISORY_LOCK_IDS)[keyof typeof ADVISORY_LO
1414

1515
const 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
1829
function 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

Comments
 (0)