{"record":{"id":"6cd7372158767b7a","repo":"remix-run/remix","slug":"mysql-migration-lock-could-not-be-acquired","errorCode":null,"errorMessage":"MySQL migration lock could not be acquired","messagePattern":"MySQL migration lock could not be acquired","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/data-table-mysql/src/lib/driver.ts","lineNumber":625,"sourceCode":"\nasync function runWithMysqlMigrationLock<result>(\n  connection: MysqlTransactionConnection,\n  name: string,\n  driver: MysqlDatabaseDriver,\n  run: (driver: DatabaseDriver<'mysql'>) => Promise<result>,\n): Promise<result> {\n  // sha2(..., 256) yields 64 hex characters, exactly GET_LOCK's 64-character\n  // lock name limit, so any additional prefix must go inside the hash input.\n  let [lockRows] = await connection.query(\n    \"select lock_name, get_lock(lock_name, 60) as `acquired` from (select sha2(concat(coalesce(database(), ''), ':', ?), 256) as lock_name) as migration_lock\",\n    [name],\n  )\n\n  let lockRow = isRowsResult(lockRows) ? lockRows[0] : undefined\n  let lockName = lockRow?.lock_name\n\n  if (typeof lockName !== 'string' || !toBooleanExists(lockRow?.acquired)) {\n    throw new Error('MySQL migration lock could not be acquired')\n  }\n\n  let outcome: { status: 'success'; value: result } | { status: 'failure'; error: unknown }\n\n  try {\n    outcome = { status: 'success', value: await run(driver) }\n  } catch (error) {\n    outcome = { status: 'failure', error }\n  }\n\n  let unlockFailed = false\n  let unlockError: unknown\n\n  try {\n    let [unlockRows] = await connection.query('select release_lock(?) as `released`', [lockName])\n\n    if (!isRowsResult(unlockRows) || !toBooleanExists(unlockRows[0]?.released)) {\n      throw new Error('MySQL migration lock was not held by the reserved connection')","sourceCodeStart":607,"sourceCodeEnd":643,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table-mysql/src/lib/driver.ts#L607-L643","documentation":"The migration lock is acquired with MySQL's GET_LOCK on a reserved connection; runWithMysqlMigrationLock throws this error when the SELECT ... FOR UPDATE / GET_LOCK probe returns a row that does not confirm acquisition (missing lock_name or acquired not truthy). It means another process holds the lock or the lock function failed unexpectedly.","triggerScenarios":"Running migrations concurrently from two processes/CI jobs against the same database; a previous migration run crashed without releasing the lock and wait_timeout has not expired; permissions or proxy issues causing GET_LOCK to return 0 or NULL.","commonSituations":"Parallel CI pipelines migrating the same shared DB; a killed migration process leaving the named lock held until the connection dies; long-running deploy overlapping another release.","solutions":["Ensure only one migration process runs at a time (deploy serialization / CI mutex)","Wait for the other holder to finish or its connection to time out, then retry","If a stale lock is suspected, manually release it with SELECT RELEASE_LOCK('<lock name>') from a privileged connection"],"exampleFix":"// before\n// two CI jobs run simultaneously:\nawait driver.runMigrations() // throws: lock could not be acquired\n// after\n// serialize migration runs (e.g. CI stage dependency or deploy lock)\nawait withDeployLock(async () => {\n  await driver.runMigrations()\n})","handlingStrategy":"retry","validationCode":"// Pre-check is not possible via GET_LOCK result; serialize instead\nconst lockKey = 'deploy-migrations'\nawait withMutex(lockKey, () => driver.runMigrations())","typeGuard":null,"tryCatchPattern":"async function migrateWithRetry(driver, attempts = 5) {\n  for (let i = 0; i < attempts; i++) {\n    try { return await driver.runMigrations() }\n    catch (e) { if (e instanceof Error && e.message.includes('could not be acquired')) { await sleep(5000); continue } throw e }\n  }\n  throw new Error('migration lock busy')\n}","preventionTips":["Serialize migration jobs across CI/deploy pipelines","Avoid pointing multiple services' startup migrations at one DB simultaneously"],"tags":["data-table","mysql","migration","lock","concurrency"],"backgroundTag":"migration-lock-contention","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}