diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index d8aa9ec7d7..2ec5ac470b 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -42,6 +42,8 @@ jobs: - name: 📥 Download deps run: pnpm install --frozen-lockfile --filter trigger.dev... + # Prisma clients generate concurrently and share one engine binary in the + # pnpm store; the generate script retries the transient Windows EPERM race. - name: 📀 Generate Prisma Client run: pnpm run generate diff --git a/internal-packages/database/package.json b/internal-packages/database/package.json index ec0bc950b8..560cf229dd 100644 --- a/internal-packages/database/package.json +++ b/internal-packages/database/package.json @@ -15,7 +15,7 @@ }, "scripts": { "clean": "rimraf dist", - "generate": "prisma generate", + "generate": "node ../../scripts/retry-prisma-generate.mjs", "db:migrate:dev:create": "prisma migrate dev --create-only", "db:migrate:deploy": "prisma migrate deploy", "db:push": "prisma db push", diff --git a/internal-packages/run-ops-database/package.json b/internal-packages/run-ops-database/package.json index cbeb0279b0..7bfe0fe271 100644 --- a/internal-packages/run-ops-database/package.json +++ b/internal-packages/run-ops-database/package.json @@ -25,7 +25,7 @@ }, "scripts": { "clean": "rimraf dist", - "generate": "prisma generate", + "generate": "node ../../scripts/retry-prisma-generate.mjs", "db:migrate:dev:create": "prisma migrate dev --create-only", "db:migrate:deploy": "node scripts/migrate.mjs deploy", "db:migrate:status": "node scripts/migrate.mjs status", diff --git a/scripts/retry-prisma-generate.mjs b/scripts/retry-prisma-generate.mjs new file mode 100644 index 0000000000..f20fa4616c --- /dev/null +++ b/scripts/retry-prisma-generate.mjs @@ -0,0 +1,53 @@ +// Retry wrapper around `prisma generate`. Our two prisma clients pin the same +// prisma version and so share one package instance in the pnpm store; when +// `turbo run generate` runs them concurrently both race to write the shared +// query-engine binary, and on Windows the loser fails with `EPERM ... rename`. +// Retrying lets it succeed once the engine file is present and unlocked. On +// non-Windows the first attempt succeeds, so this is a zero-cost no-op. +import { spawnSync } from "node:child_process"; + +const MAX_ATTEMPTS = 5; +const BASE_DELAY_MS = 500; + +// Transient, retryable filesystem contention on the shared engine binary. +const TRANSIENT = + /\b(EPERM|EBUSY|EACCES)\b|operation not permitted|resource busy or locked|being used by another process/i; + +const passthroughArgs = process.argv.slice(2); + +function sleepSync(ms) { + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); +} + +let lastStatus = 1; + +for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + const result = spawnSync("prisma", ["generate", ...passthroughArgs], { + shell: true, + encoding: "utf8", + }); + + process.stdout.write(result.stdout ?? ""); + process.stderr.write(result.stderr ?? ""); + + if (result.status === 0) { + process.exit(0); + } + + lastStatus = result.status ?? 1; + + const output = `${result.stdout ?? ""}${result.stderr ?? ""}`; + const isRetryable = TRANSIENT.test(output); + + if (!isRetryable || attempt === MAX_ATTEMPTS) { + break; + } + + const delay = BASE_DELAY_MS * attempt; + console.error( + `prisma generate hit a transient filesystem error (attempt ${attempt}/${MAX_ATTEMPTS}); retrying in ${delay}ms...` + ); + sleepSync(delay); +} + +process.exit(lastStatus);