← Blog
Migration locks for TypeORM
- Databases
- NodeJS
- TypeORM
- TypeScript
Schema migrations is a must-have functionality for any DB framework.
TypeORM provides decent utilities for dealing with migrations, however, having a decade of experience in Ruby on Rails, I got really spoiled and take some features for granted.
One of these features is locking a database while a migration is going on so 2 processes running concurrently don’t step on each other’s toes. This is also important when you run migrations in Kubernetes before launching your app.
I was really surprised to find out that this basic feature is not supported, so I decided to implement it on my own.
Implementation
// typeormMigrationUtils.ts
import { Connection, createConnection } from "typeorm"
import config from "../ormconfig"
import CRC32 from "crc-32"
const MIGRATOR_SALT = 2053462845
async function withAdvisoryLock(
connection: Connection,
callback: () => Promise<void>,
): Promise<boolean> {
// generate a unique lock name, has to be an integer
const lockName = CRC32.str(config.database as string) * MIGRATOR_SALT
let lock = false
try {
// try to acquire a lock
const [{ pg_try_advisory_lock: locked }]: [{ pg_try_advisory_lock: boolean }] =
await connection.manager.query(`SELECT pg_try_advisory_lock(${lockName})`)
lock = locked
// if already locked, print a warning an exit
if (!lock) {
console.warn(`Failed to get advisory lock: ${lockName}`)
return false
}
// execute our code inside the lock
await callback()
return true
} finally {
// if we acquired a lock, we need to unlock it
if (lock) {
const [{ pg_advisory_unlock: wasLocked }]: [{ pg_advisory_unlock: boolean }] =
await connection.manager.query(`SELECT pg_advisory_unlock(${lockName})`)
if (!wasLocked) {
console.warn(`Advisory lock was not locked: ${lockName}`)
}
}
}
}
export async function migrateDatabase() {
const connection = await createConnection({ ...config, logging: true })
await withAdvisoryLock(connection, async () => {
await connection.runMigrations({
transaction: "all",
})
})
await connection.close()
}
export async function syncDatabase() {
const connection = await createConnection({ ...config, logging: true })
await withAdvisoryLock(connection, async () => {
await connection.synchronize()
})
await connection.close()
}
Usage
You can run them like this:
// package.json
"scripts": {
...
"db:migrate": "ts-node ./src/scripts/migrateDatabase.ts",
"db:sync": "ts-node ./src/scripts/syncDatabase.ts",
"db:migrate:prod": "node ./dist/src/scripts/migrateDatabase.js",
"db:sync:prod": "node ./dist/src/scripts/syncDatabase.js",
...
},
// migrateDatabase.ts
import { migrateDatabase } from "./typeormMigrationUtils"
;(async () => {
await migrateDatabase()
})()
// syncDatabase.ts
import { syncDatabase } from "./typeormMigrationUtils"
;(async () => {
await syncDatabase()
})()