* Run pending migrations, retrying on lock timeout (55P03, found anywhere in * the wrapped `cause` chain). Each attempt re-verifies the lock session (pid) * and re-asserts the session timeouts — a migration file may have changed them, * and `SET` cannot be parameterized, hence `client.unsafe` wit
()
| 180 | * CONCURRENTLY statements are idempotent by convention. |
| 181 | */ |
| 182 | async function runMigrationsWithRetry(): Promise<void> { |
| 183 | for (let attempt = 1; ; attempt++) { |
| 184 | if (hasDirectMigrationUrl) { |
| 185 | const [{ pid }] = await client`SELECT pg_backend_pid() AS pid` |
| 186 | if (pid !== lockSessionPid) { |
| 187 | throw new Error( |
| 188 | `Database session changed mid-run (backend pid ${lockSessionPid} -> ${pid}); ` + |
| 189 | 'the migration advisory lock was lost. Aborting so a fresh runner can retry safely.' |
| 190 | ) |
| 191 | } |
| 192 | } |
| 193 | await client.unsafe('SET statement_timeout = 0') |
| 194 | await client.unsafe(`SET lock_timeout = '${DDL_LOCK_TIMEOUT}'`) |
| 195 | try { |
| 196 | await migrate(drizzle(client), { migrationsFolder: './migrations' }) |
| 197 | return |
| 198 | } catch (error) { |
| 199 | const isLockTimeout = getPostgresErrorCode(error) === '55P03' |
| 200 | if (!isLockTimeout || attempt >= MAX_MIGRATE_ATTEMPTS) throw error |
| 201 | const delayMs = backoffWithJitter(attempt, null, MIGRATE_RETRY_BACKOFF) |
| 202 | console.warn( |
| 203 | `WARN: migration DDL hit lock_timeout (attempt ${attempt}/${MAX_MIGRATE_ATTEMPTS}); ` + |
| 204 | `retrying in ${Math.round(delayMs)}ms.` |
| 205 | ) |
| 206 | await sleep(delayMs) |
| 207 | } |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | /** |
| 212 | * A failed CONCURRENTLY build leaves an INVALID index that `IF NOT EXISTS` |
no test coverage detected