Allow terminating idle migration blockers

This commit is contained in:
2026-07-14 16:58:01 +02:00
parent d0f5a72350
commit 3153a7a669

View File

@@ -12,31 +12,40 @@ const logStep = (message: string) => {
console.log(`[tenant-lock-migration] ${message}`) console.log(`[tenant-lock-migration] ${message}`)
} }
const printRelationLocks = async (db: ReturnType<typeof drizzle>, relationName: string) => { const terminateIdleBlockers = process.argv.includes("--terminate-idle-blockers")
const result = await db.execute(sql`
SELECT
activity.pid,
activity.state,
activity.wait_event_type,
activity.wait_event,
now() - activity.query_start AS query_age,
locks.mode,
locks.granted,
left(activity.query, 500) AS query
FROM pg_locks locks
JOIN pg_class relation ON relation.oid = locks.relation
LEFT JOIN pg_stat_activity activity ON activity.pid = locks.pid
WHERE relation.relname = ${relationName}
ORDER BY locks.granted ASC, activity.query_start ASC NULLS LAST;
`)
if (!result.rows.length) { const relationLocksQuery = (relationName: string) => sql`
SELECT
activity.pid,
activity.state,
activity.wait_event_type,
activity.wait_event,
now() - activity.query_start AS query_age,
locks.mode,
locks.granted,
left(activity.query, 500) AS query
FROM pg_locks locks
JOIN pg_class relation ON relation.oid = locks.relation
LEFT JOIN pg_stat_activity activity ON activity.pid = locks.pid
WHERE relation.relname = ${relationName}
ORDER BY locks.granted ASC, activity.query_start ASC NULLS LAST;
`
const getRelationLocks = async (db: ReturnType<typeof drizzle>, relationName: string) => {
const result = await db.execute(relationLocksQuery(relationName))
return result.rows as Record<string, any>[]
}
const printRelationLocks = async (db: ReturnType<typeof drizzle>, relationName: string) => {
const rows = await getRelationLocks(db, relationName)
if (!rows.length) {
logStep(`Keine aktiven Locks fuer ${relationName} gefunden`) logStep(`Keine aktiven Locks fuer ${relationName} gefunden`)
return return
} }
logStep(`Aktive Locks fuer ${relationName}:`) logStep(`Aktive Locks fuer ${relationName}:`)
for (const row of result.rows as Record<string, any>[]) { for (const row of rows) {
console.log(JSON.stringify({ console.log(JSON.stringify({
pid: row.pid, pid: row.pid,
state: row.state, state: row.state,
@@ -50,6 +59,54 @@ const printRelationLocks = async (db: ReturnType<typeof drizzle>, relationName:
} }
} }
const terminateIdleRelationBlockers = async (db: ReturnType<typeof drizzle>, relationName: string) => {
const rows = await getRelationLocks(db, relationName)
const blockerPids = rows
.filter((row) =>
row.granted === true &&
row.state === "idle in transaction" &&
Number(row.pid) !== process.pid
)
.map((row) => Number(row.pid))
.filter((pid) => Number.isFinite(pid))
if (!blockerPids.length) {
logStep(`Keine idle-in-transaction Blocker fuer ${relationName} gefunden`)
return false
}
for (const pid of blockerPids) {
logStep(`Beende idle-in-transaction DB-Session ${pid}`)
await db.execute(sql`SELECT pg_terminate_backend(${pid})`)
}
return true
}
const executeStatement = async (
db: ReturnType<typeof drizzle>,
statement: ReturnType<typeof sql>,
) => {
await db.execute(statement)
}
const handleLockError = async (
db: ReturnType<typeof drizzle>,
relationName: string,
) => {
logStep(`Konnte Lock fuer ${relationName} nicht rechtzeitig bekommen`)
await printRelationLocks(db, relationName)
if (!terminateIdleBlockers) return false
const terminated = await terminateIdleRelationBlockers(db, relationName)
if (terminated) {
logStep(`Retry nach beendeten idle-in-transaction Sessions fuer ${relationName}`)
}
return terminated
}
const executeStep = async ( const executeStep = async (
db: ReturnType<typeof drizzle>, db: ReturnType<typeof drizzle>,
message: string, message: string,
@@ -59,19 +116,25 @@ const executeStep = async (
logStep(message) logStep(message)
try { try {
await db.execute(statement) await executeStatement(db, statement)
} catch (err: any) { } catch (err: any) {
const code = err?.cause?.code || err?.code const code = err?.cause?.code || err?.code
if (lockRelationName && ["55P03", "57014"].includes(code)) { if (!lockRelationName || !["55P03", "57014"].includes(code)) {
logStep(`Konnte Lock fuer ${lockRelationName} nicht rechtzeitig bekommen`) throw err
await printRelationLocks(db, lockRelationName)
} }
throw err const shouldRetry = await handleLockError(db, lockRelationName)
if (!shouldRetry) throw err
await executeStatement(db, statement)
} }
} }
async function run() { async function run() {
if (terminateIdleBlockers) {
logStep("Option --terminate-idle-blockers aktiv")
}
let connectionString = process.env.DATABASE_URL let connectionString = process.env.DATABASE_URL
if (!connectionString) { if (!connectionString) {