mirror of
https://github.com/supabase/supabase.git
synced 2026-10-06 01:45:10 +03:00
Table editor tests were being run serially because they would otherwise stomp all over each other in setup. But this made them really slow. This PR changes them to run in parallel, and makes sure the setup step runs only once by using a file lock. Also cleans up miscellaneous other sources of flakiness in tests. Co-authored-by: Ali Waseem <waseema393@gmail.com>
176 lines
4.5 KiB
TypeScript
176 lines
4.5 KiB
TypeScript
import crypto from 'node:crypto'
|
|
import fs, { type FileHandle } from 'node:fs/promises'
|
|
import os from 'node:os'
|
|
import path from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const ROOT = path.join(os.tmpdir(), 'playwright-locks')
|
|
|
|
const sha1 = (str: string) => crypto.createHash('sha1').update(str).digest('hex')
|
|
const uniqueWorkerId = () => {
|
|
const workerIndex = process.env.TEST_WORKER_INDEX
|
|
const hostname = os.hostname()
|
|
const pid = process.pid
|
|
const uniquePart = crypto.randomBytes(4).toString('hex')
|
|
|
|
return `${hostname}-${pid}-${workerIndex}-${uniquePart}`
|
|
}
|
|
|
|
const keyFromModuleUrl = (moduleUrl: string) => {
|
|
const absPath = fileURLToPath(moduleUrl)
|
|
const key = sha1(absPath)
|
|
const dir = path.join(ROOT, key.slice(0, 2), key.slice(2))
|
|
return { absPath, key, dir }
|
|
}
|
|
|
|
const exists = async (path: string) => {
|
|
try {
|
|
await fs.access(path)
|
|
return true
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
const getCodeFromUnknownError = (err: unknown): string | undefined => {
|
|
if (err && typeof err === 'object' && 'code' in err) {
|
|
return String(err.code)
|
|
}
|
|
}
|
|
|
|
export const withFileOnceSetup = async (
|
|
currentModuleUrl: string,
|
|
fn: () => Promise<void>,
|
|
{ timeoutMs = 120_000 } = {}
|
|
): Promise<void> => {
|
|
const { absPath, dir } = keyFromModuleUrl(currentModuleUrl)
|
|
const lockfile = path.join(dir, `setup.lock`)
|
|
const doneFile = path.join(dir, `setup.done.json`)
|
|
const leasesDir = path.join(dir, `leases`)
|
|
const leaseId = uniqueWorkerId()
|
|
const myLease = path.join(leasesDir, leaseId)
|
|
|
|
await fs.mkdir(leasesDir, { recursive: true })
|
|
|
|
// Acquire a lease for this worker on this test file
|
|
await fs.writeFile(myLease, '')
|
|
|
|
// Fast path: another worker already finished setup
|
|
if (await exists(doneFile)) {
|
|
return
|
|
}
|
|
|
|
// Try to acquire the setup lock
|
|
let fd: FileHandle | null = null
|
|
try {
|
|
fd = await fs.open(lockfile, 'wx')
|
|
} catch (err: unknown) {
|
|
if (getCodeFromUnknownError(err) !== 'EEXIST') {
|
|
// Drop lease and bail
|
|
try {
|
|
await fs.unlink(myLease)
|
|
} catch {}
|
|
throw err
|
|
}
|
|
|
|
// Another worker is doing setup, wait for done
|
|
const start = Date.now()
|
|
const baseSleepMs = 500
|
|
const incrementMs = 100
|
|
let attempt = 0
|
|
|
|
while (!(await exists(doneFile))) {
|
|
if (Date.now() - start > timeoutMs) {
|
|
// Drop lease and bail
|
|
try {
|
|
await fs.unlink(myLease)
|
|
} catch {}
|
|
throw new Error(`Timed out waiting for once-per-file setup in ${absPath}`)
|
|
}
|
|
|
|
const jitter = (Math.random() - 0.5) * 100
|
|
const sleep = baseSleepMs + incrementMs * Math.pow(2, attempt++) + jitter
|
|
await new Promise((resolve) => setTimeout(resolve, sleep))
|
|
}
|
|
}
|
|
|
|
// We have the lock, perform setup and mark done
|
|
try {
|
|
if (!(await exists(doneFile))) {
|
|
await fn()
|
|
await fs.writeFile(
|
|
doneFile,
|
|
JSON.stringify({ at: new Date().toISOString(), file: absPath, pid: process.pid }, null, 2)
|
|
)
|
|
}
|
|
} finally {
|
|
try {
|
|
await fd?.close()
|
|
} catch {}
|
|
try {
|
|
await fs.unlink(lockfile)
|
|
} catch {}
|
|
}
|
|
}
|
|
|
|
export const releaseFileOnceCleanup = async (currentModuleUrl: string): Promise<void> => {
|
|
const { dir } = keyFromModuleUrl(currentModuleUrl)
|
|
const leasesDir = path.join(dir, `leases`)
|
|
const doneFile = path.join(dir, `setup.done.json`)
|
|
const cleanupLock = path.join(dir, `cleanup.lock`)
|
|
|
|
// Remove all leases associated with this worker
|
|
try {
|
|
const entries = await fs.readdir(leasesDir)
|
|
await Promise.all(
|
|
entries.map(async (name) => {
|
|
if (name.includes(`-${process.pid}-`)) {
|
|
try {
|
|
await fs.unlink(path.join(leasesDir, name))
|
|
} catch {}
|
|
}
|
|
})
|
|
)
|
|
} catch {}
|
|
|
|
// Try to acquire cleanup lock
|
|
let fd: FileHandle | null = null
|
|
try {
|
|
await fs.mkdir(dir, { recursive: true })
|
|
fd = await fs.open(cleanupLock, 'wx')
|
|
} catch {
|
|
// Another worker is cleaning up; we're done
|
|
return
|
|
}
|
|
|
|
try {
|
|
// Only delete if no leases remain
|
|
let remaining = 0
|
|
try {
|
|
const entries = await fs.readdir(leasesDir)
|
|
remaining = entries.length
|
|
} catch {}
|
|
|
|
if (remaining === 0) {
|
|
// Remove done marker and empty directories
|
|
try {
|
|
await fs.unlink(doneFile)
|
|
} catch {}
|
|
try {
|
|
await fs.rmdir(leasesDir)
|
|
} catch {}
|
|
// Attempt to remove the key directory if empty
|
|
try {
|
|
await fs.rmdir(dir)
|
|
} catch {}
|
|
}
|
|
} finally {
|
|
try {
|
|
await fd?.close()
|
|
} catch {}
|
|
try {
|
|
await fs.unlink(cleanupLock)
|
|
} catch {}
|
|
}
|
|
}
|