From f08d33e7c90ba932b089abab5b12b9913e19ffc4 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 30 Sep 2026 08:25:26 -0700 Subject: [PATCH] fix(hub): migrate holds a session-level advisory lock, not a transaction (CL-9618) --- apps/hub/src/migrate.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/hub/src/migrate.ts b/apps/hub/src/migrate.ts index 37e8e29cb..ccd55eb18 100644 --- a/apps/hub/src/migrate.ts +++ b/apps/hub/src/migrate.ts @@ -7,6 +7,7 @@ // service, no ledger table, just the hub doing the work itself. import { runMigrations } from "@intx/db"; import { sql } from "drizzle-orm"; +import postgres from "postgres"; import { applyAgentTokenMigrations } from "@corbits/agent-token/migrations"; import { applyCronMigrations } from "@corbits/cron"; import { createMailboxDb, runMailboxMigrations } from "@corbits/mailbox"; @@ -52,13 +53,20 @@ export async function migrateHub( config: HubMigrateConfig, db: Parameters[0], ): Promise { - // Concurrent boots against one database (parallel e2e suites, rolling - // deploys) would race on the one-shot platform SQL's CREATE TYPE. The - // transaction-scoped advisory lock serializes whole migrations. - await db.transaction(async (tx) => { - await tx.execute(sql`select pg_advisory_xact_lock(${MIGRATE_LOCK_KEY})`); - await applyMigrations(config, db); - }); + // Concurrent boots serialize on a session-level advisory lock held by a + // dedicated connection. No transaction stays open, so a single-backend + // database like PGlite still serves the migrations' other connections. + const lockClient = postgres(databaseUrlFrom(config), { max: 1 }); + try { + await lockClient`select pg_advisory_lock(${MIGRATE_LOCK_KEY})`; + try { + await applyMigrations(config, db); + } finally { + await lockClient`select pg_advisory_unlock(${MIGRATE_LOCK_KEY})`; + } + } finally { + await lockClient.end(); + } } const MIGRATE_LOCK_KEY = 7_460_391_205;