Added database lock

This commit is contained in:
Jody Doolittle 2025-07-29 14:53:11 -07:00
parent a52376b4ed
commit cb2449ce58
3 changed files with 339 additions and 15 deletions

View file

@ -31,8 +31,8 @@
"format": "prettier --write .",
"lint": "prettier --check .",
"db:generate": "drizzle-kit generate --config ./src/lib/server/db/drizzle.config.ts",
"db:migrate": "drizzle-kit migrate --config ./src/lib/server/db/drizzle.config.ts",
"db:studio": "drizzle-kit studio --config ./src/lib/server/db/drizzle.config.ts",
"db:migrate": "node scripts/check-db-lock.js drizzle-kit migrate --config ./src/lib/server/db/drizzle.config.ts",
"db:studio": "node scripts/check-db-lock.js drizzle-kit studio --config ./src/lib/server/db/drizzle.config.ts",
"bundle": "node scripts/bundle-dist.js",
"dist": "npm run build && npm run bundle"
},

215
scripts/check-db-lock.js Normal file
View file

@ -0,0 +1,215 @@
#!/usr/bin/env node
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
import { spawn } from 'child_process'
const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
// Get the project root directory
const projectRoot = path.resolve(__dirname, '..')
// Lock configuration
const DEFAULT_LOCK_LENGTH = 5000 // 5 seconds in milliseconds
let lockUpdateInterval = null
let metaPath = null
let lockReleased = false
// Import the drizzle config to get the data directory
async function importDrizzleConfig() {
try {
const configPath = path.join(projectRoot, 'src/lib/server/db/drizzle.config.ts')
return new Promise((resolve, reject) => {
const child = spawn('npx', ['tsx', '--eval', `
import * as config from '${configPath}';
console.log(JSON.stringify({ dataDir: config.dataDir }));
`], { stdio: ['pipe', 'pipe', 'pipe'] })
let output = ''
child.stdout.on('data', (data) => {
output += data.toString()
})
child.on('close', (code) => {
if (code === 0) {
try {
const lines = output.trim().split('\n')
const jsonLine = lines[lines.length - 1]
const config = JSON.parse(jsonLine)
resolve(config)
} catch (error) {
reject(new Error(`Failed to parse config output: ${error.message}`))
}
} else {
reject(new Error(`Failed to load drizzle config, exit code: ${code}`))
}
})
})
} catch (error) {
console.error('Failed to import drizzle config:', error.message)
process.exit(1)
}
}
function updateDatabaseLock() {
try {
// Read current meta.json
let meta = { version: "0.0.0" }
if (fs.existsSync(metaPath)) {
meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'))
}
// Update lock
meta.lock = {
timestamp: Date.now(),
lockLength: DEFAULT_LOCK_LENGTH
}
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2))
} catch (error) {
console.error('Failed to update database lock:', error.message)
}
}
function startLockUpdates() {
// Update lock immediately
updateDatabaseLock()
console.log('Database locked for db operation.')
// Set up interval to update lock every few seconds
lockUpdateInterval = setInterval(() => {
updateDatabaseLock()
}, DEFAULT_LOCK_LENGTH - 1000) // Update 1 second before lock expires
}
function stopLockUpdates() {
if (lockReleased) {
return // Already cleaned up
}
lockReleased = true
if (lockUpdateInterval) {
clearInterval(lockUpdateInterval)
lockUpdateInterval = null
}
// Clear the lock
try {
if (fs.existsSync(metaPath)) {
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'))
delete meta.lock
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2))
console.log('\nDatabase lock released.')
}
} catch (error) {
console.error('Failed to clear database lock:', error.message)
}
}
async function checkDatabaseLock() {
try {
const config = await importDrizzleConfig()
metaPath = path.join(config.dataDir, 'meta.json')
// Check if meta.json exists
if (!fs.existsSync(metaPath)) {
console.log('meta.json does not exist. Continuing...')
return
}
// Read meta.json
let meta
try {
meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'))
} catch (error) {
console.error('Failed to read meta.json:', error.message)
process.exit(1)
}
// Check if lock exists
if (!meta.lock) {
console.log('No database lock found. Continuing...')
return
}
const currentTime = Date.now()
const lockExpiry = meta.lock.timestamp + meta.lock.lockLength
if (currentTime < lockExpiry) {
// Lock is still active
const remainingTime = Math.ceil((lockExpiry - currentTime) / 1000)
console.error(`Database is currently locked. Lock expires in ${remainingTime} seconds.`)
console.error('Please wait for the lock to expire or stop the running application.')
process.exit(1)
} else {
// Lock is stale
console.log('Found stale database lock. Continuing...')
}
} catch (error) {
console.error('Error checking database lock:', error.message)
process.exit(1)
}
}
async function runWithLock() {
// Get command line arguments (everything after the script name)
const args = process.argv.slice(2)
if (args.length === 0) {
console.error('No command provided to run with lock')
process.exit(1)
}
try {
// Check for existing lock first
await checkDatabaseLock()
// Start maintaining our lock
startLockUpdates()
// Set up cleanup handlers
process.on('exit', stopLockUpdates)
process.on('SIGINT', () => {
stopLockUpdates()
process.exit(0)
})
process.on('SIGTERM', () => {
stopLockUpdates()
process.exit(0)
})
// Run the actual command
const command = args[0]
const commandArgs = args.slice(1)
console.log(`Running: ${command} ${commandArgs.join(' ')}`)
const child = spawn(command, commandArgs, {
stdio: 'inherit',
shell: true
})
child.on('close', (code) => {
stopLockUpdates()
process.exit(code)
})
child.on('error', (error) => {
console.error('Failed to start command:', error.message)
stopLockUpdates()
process.exit(1)
})
} catch (error) {
console.error('Error running command with lock:', error.message)
stopLockUpdates()
process.exit(1)
}
}
// Run the command with lock
runWithLock()

View file

@ -7,6 +7,128 @@ import { dev } from "$app/environment"
import { drizzle } from "drizzle-orm/pglite"
import { sync } from "./defaults"
// Database lock interface
interface DbLock {
timestamp: number
lockLength: number // in milliseconds
}
interface MetaFile {
version: string
lock?: DbLock
}
// Move meta.json handling to the beginning
const metaPath = dbConfig.dataDir + "/meta.json"
// Ensure meta.json exists
if (!fs.existsSync(metaPath)) {
fs.writeFileSync(
metaPath,
JSON.stringify({ version: "0.0.0" }, null, 2)
)
}
// Read meta.json
let meta: MetaFile = JSON.parse(fs.readFileSync(metaPath, "utf-8"))
// Database lock functions
const DEFAULT_LOCK_LENGTH = 5000 // 5 seconds in milliseconds
async function checkDatabaseLock(): Promise<void> {
// Refresh meta from file
meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"))
if (!meta.lock) {
// No lock exists, continue
return
}
const currentTime = Date.now()
const lockExpiry = meta.lock.timestamp + meta.lock.lockLength
if (currentTime < lockExpiry) {
// Lock is still active, wait for it to expire
const waitTime = lockExpiry - currentTime
console.log(`Database locked, waiting ${waitTime}ms for lock to expire...`)
await new Promise(resolve => setTimeout(resolve, waitTime))
// Check again after waiting
meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"))
if (meta.lock && Date.now() < meta.lock.timestamp + meta.lock.lockLength) {
// Still locked after waiting, exit application
console.error("Database remains locked after waiting. Exiting application.")
process.exit(1)
}
}
// Lock is stale or doesn't exist, continue
}
function updateDatabaseLock(): void {
try {
// Refresh meta from file
meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"))
meta.lock = {
timestamp: Date.now(),
lockLength: DEFAULT_LOCK_LENGTH
}
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2))
} catch (error) {
console.error("Failed to update database lock:", error)
}
}
// Background lock update function
let lockUpdateInterval: NodeJS.Timeout | null = null
function startLockUpdates(): void {
// Update lock immediately
updateDatabaseLock()
// Set up interval to update lock every few seconds
lockUpdateInterval = setInterval(() => {
updateDatabaseLock()
}, DEFAULT_LOCK_LENGTH - 1000) // Update 1 second before lock expires
}
function stopLockUpdates(): void {
if (lockUpdateInterval) {
clearInterval(lockUpdateInterval)
lockUpdateInterval = null
}
// Clear the lock when stopping
try {
meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"))
delete meta.lock
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2))
} catch (error) {
console.error("Failed to clear database lock:", error)
}
}
// Clean up lock on process exit
process.on('exit', stopLockUpdates)
process.on('SIGINT', () => {
stopLockUpdates()
process.exit(0)
})
process.on('SIGTERM', () => {
stopLockUpdates()
process.exit(0)
})
// Check database lock before proceeding
await checkDatabaseLock()
// Start lock updates
startLockUpdates()
// const { firstInit, pglite } = await startPg()
export let db = drizzle(dbConfig.dbPath, { schema })
@ -48,19 +170,6 @@ try {
// Run migrations if in production environment
if (!dev || !hasTables) {
// If it doesn't exist, create a meta.json file in the data directory
const metaPath = dbConfig.dataDir + "/meta.json"
// Check if the file exists
if (!fs.existsSync(metaPath)) {
// Create the file with default content
fs.writeFileSync(
metaPath,
JSON.stringify({ version: "0.0.0" }, null, 2)
)
}
// Check meta.json for version
const meta = JSON.parse(fs.readFileSync(metaPath, "utf-8"))
// @ts-ignore
const appVersion = __APP_VERSION__
if (!appVersion) {