{"record":{"id":"464f939e42729c92","repo":"remix-run/remix","slug":"mysql-migration-lock-is-already-held-by-this-datab","errorCode":null,"errorMessage":"MySQL migration lock is already held by this database","messagePattern":"MySQL migration lock is already held by this database","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/data-table-mysql/src/lib/driver.ts","lineNumber":407,"sourceCode":"  }\n\n  /**\n   * Runs migration work on the mysql connection that owns the named lock.\n   *\n   * Lock acquisition waits up to 60 seconds and throws when the lock cannot\n   * be acquired. Re-entering this method from inside `run` throws instead of\n   * deadlocking, and a failed run destroys the reserved connection instead of\n   * returning it to the pool.\n   * @param name Logical migration lock name.\n   * @param run Migration work to run with a connection-bound driver.\n   * @returns The callback result.\n   */\n  async withMigrationLock<result>(\n    name: string,\n    run: (driver: DatabaseDriver<'mysql'>) => Promise<result>,\n  ): Promise<result> {\n    if (this.#migrationLockStore.getStore()) {\n      throw new Error('MySQL migration lock is already held by this database')\n    }\n\n    let waitForPreviousLock = this.#migrationLockQueue\n    let releaseQueue: () => void = () => undefined\n    this.#migrationLockQueue = new Promise((resolve) => {\n      releaseQueue = resolve\n    })\n\n    await waitForPreviousLock\n\n    try {\n      let releaseOnClose = false\n      let connection: MysqlTransactionConnection\n\n      if (isMysqlPool(this.#client)) {\n        connection = await this.#client.getConnection()\n        releaseOnClose = true\n      } else {","sourceCodeStart":389,"sourceCodeEnd":425,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table-mysql/src/lib/driver.ts#L389-L425","documentation":"The MySQL driver serializes migration lock holders via an AsyncLocalStorage store plus a promise queue; withMigrationLock throws if the current async context already holds the lock. This guards against re-entrant/nested migration lock acquisition on the same database, which would otherwise deadlock on MySQL's named locks.","triggerScenarios":"Calling withMigrationLock (directly or via a migration runner) from within a callback already running under withMigrationLock on the same driver instance — e.g. nested migration invocations or a migration that itself triggers the lock-protected runner.","commonSituations":"Custom migration orchestration that wraps the official runner in another withMigrationLock call; test fixtures that run migrations inside a locked setup block; recursive migration scripts.","solutions":["Remove the nested withMigrationLock call and let the outer holder own the lock","Restructure so migrations run sequentially under a single lock acquisition","If composing runners, check whether the lock is already held before acquiring (e.g. expose/acquire a store check) instead of unconditionally re-locking"],"exampleFix":"// before\nawait driver.withMigrationLock('migrate', async () => {\n  await runMigrations(driver) // internally calls withMigrationLock again\n})\n// after\nawait runMigrations(driver) // single lock acquisition inside the runner","handlingStrategy":"validation","validationCode":"// Serialize migration calls yourself so nesting can't happen\nlet migrating = false\nasync function migrateOnce(driver) {\n  if (migrating) throw new Error('migration already running')\n  migrating = true\n  try { await runMigrations(driver) } finally { migrating = false }\n}","typeGuard":null,"tryCatchPattern":"try { await driver.withMigrationLock(name, run) } catch (e) { if (e instanceof Error && e.message.includes('already held')) { return run(driver) /* trust outer holder */ } throw e }","preventionTips":["Never wrap the official migration runner in your own lock","Run migrations from a single orchestration entry point"],"tags":["data-table","mysql","migration","lock","deadlock"],"backgroundTag":"nested-lock-acquisition","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}