diff --git a/src/lib/postgres.mjs b/src/lib/postgres.mjs index 2b26a3d..8f14711 100644 --- a/src/lib/postgres.mjs +++ b/src/lib/postgres.mjs @@ -126,22 +126,32 @@ export async function ensureHistorySchema(pool) { raw JSONB ) `); - await pool.query(` - CREATE INDEX IF NOT EXISTS ${table}_quote_id_idx - ON ${table} (quote_id) - `); - await pool.query(` - CREATE INDEX IF NOT EXISTS ${table}_decision_key_idx - ON ${table} (decision_key) - `); - await pool.query(` - CREATE INDEX IF NOT EXISTS ${table}_ingested_at_idx - ON ${table} (ingested_at DESC) - `); - await pool.query(` - CREATE INDEX IF NOT EXISTS ${table}_coalesced_at_idx - ON ${table} (COALESCE(observed_at, ingested_at) DESC) - `); + // Use a short lock_timeout so CREATE INDEX IF NOT EXISTS doesn't block + // startup indefinitely when another session holds a conflicting lock. + // The indexes already exist in steady state, so a timeout is harmless. + for (const indexSql of [ + `CREATE INDEX IF NOT EXISTS ${table}_quote_id_idx ON ${table} (quote_id)`, + `CREATE INDEX IF NOT EXISTS ${table}_decision_key_idx ON ${table} (decision_key)`, + `CREATE INDEX IF NOT EXISTS ${table}_ingested_at_idx ON ${table} (ingested_at DESC)`, + `CREATE INDEX IF NOT EXISTS ${table}_coalesced_at_idx ON ${table} (COALESCE(observed_at, ingested_at) DESC)`, + ]) { + const client = await pool.connect(); + try { + await client.query(`SET lock_timeout = '5s'`); + await client.query(indexSql); + } catch (error) { + if (error?.code === '55P03') { + // lock_not_available — another session holds the lock, skip + continue; + } + if (isConcurrentSchemaIndexExistsError(error)) continue; + throw error; + } finally { + // Reset lock_timeout before returning client to pool + try { await client.query(`SET lock_timeout = '0'`); } catch { /* ignore */ } + client.release(); + } + } } await ensureExpressionIndex(pool, { @@ -6448,11 +6458,17 @@ async function ensureExpressionIndex(pool, { name, table, expression }) { } export async function ensureSchemaIndex(pool, sql) { + const client = await pool.connect(); try { - await pool.query(sql); + await client.query(`SET lock_timeout = '5s'`); + await client.query(sql); } catch (error) { if (isConcurrentSchemaIndexExistsError(error)) return; + if (error?.code === '55P03') return; // lock_not_available — skip gracefully throw error; + } finally { + try { await client.query(`SET lock_timeout = '0'`); } catch { /* ignore */ } + client.release(); } }