test: Backend-e2e in CI stabil (eigene Jahre, Sperren, ein Server je App)
- daily-evaluation nutzt die Jahre 2011/2012 statt 2001, damit der Job nicht die Daten von autopilot und reminders liest und verändert. - throttler: app.listen(0) einmal je App statt eines Servers je Anfrage (ECONNRESET bei 40 parallelen Anfragen). - outbox-claims: 120 s Zeitlimit für 2000 sequentielle Anweisungen. - migration-harness: eine `migrate deploy` zur Zeit über eine eigene Advisory-Sperre (P1002 bei parallelen Migrations-Suites). - authority-notices und authority-notices-herd laufen über exclusive-suite nacheinander; Herd-Wiederholung räumt eigene Entwürfe. Refs #32 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
258e90828f
commit
6edae3efec
7 files changed
+217
-140
No files matched your search
@@ -0,0 +1,31 @@
|
||||
import { Client } from 'pg';
|
||||
|
||||
/** Arbitrary keys, one per shared resource a suite needs to own alone. */
|
||||
export const EXCLUSIVE_KEYS = {
|
||||
/** Open drafts are unique per kind: only one suite may create/clear them. */
|
||||
authorityNotices: 72707371,
|
||||
} as const;
|
||||
|
||||
/**
|
||||
* Holds a Postgres session advisory lock for the lifetime of a suite, so
|
||||
* suites that clear or count a table whose rows are global (e.g. the open
|
||||
* draft per notice kind) never run at the same time, even in parallel jest
|
||||
* workers. Blocks until the previous owner released it; the lock disappears
|
||||
* with the connection if a worker dies. Call release() in afterAll.
|
||||
*/
|
||||
export async function acquireExclusive(
|
||||
key: number,
|
||||
): Promise<() => Promise<void>> {
|
||||
const url = process.env.DATABASE_URL;
|
||||
if (!url) {
|
||||
throw new Error('DATABASE_URL is not set');
|
||||
}
|
||||
const connection = new URL(url);
|
||||
connection.searchParams.delete('schema');
|
||||
const client = new Client({ connectionString: connection.toString() });
|
||||
await client.connect();
|
||||
await client.query('SELECT pg_advisory_lock($1)', [key]);
|
||||
return async () => {
|
||||
await client.end();
|
||||
};
|
||||
}
|
||||
@@ -19,6 +19,15 @@ const PRISMA_CLI = path.join(
|
||||
'index.js',
|
||||
);
|
||||
const DEPLOY_TIMEOUT_MS = 100_000;
|
||||
/**
|
||||
* `prisma migrate deploy` takes pg_advisory_lock(72707369) and gives up after
|
||||
* 10 s. Advisory locks are per DATABASE, not per schema, so scratch schemas
|
||||
* of parallel migration suites (honey-ledger, t4, t6) collide on it (P1002 in
|
||||
* CI). The harness therefore runs one deploy at a time across jest workers
|
||||
* with its own, blocking advisory lock; the session lock disappears with the
|
||||
* connection should a worker die.
|
||||
*/
|
||||
const HARNESS_DEPLOY_LOCK_KEY = 72707370;
|
||||
const SCHEMA_NAME_PATTERN = /^mig_[0-9a-f]{12}$/;
|
||||
|
||||
export interface ScratchDatabase {
|
||||
@@ -119,21 +128,28 @@ export async function createScratchDatabase(
|
||||
copied.add(name);
|
||||
}
|
||||
}
|
||||
await execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
PRISMA_CLI,
|
||||
'migrate',
|
||||
'deploy',
|
||||
'--config',
|
||||
path.join(workDir, 'prisma.config.ts'),
|
||||
],
|
||||
{
|
||||
cwd: BACKEND_ROOT,
|
||||
env: { ...process.env, MIGRATION_HARNESS_URL: scratchUrl },
|
||||
timeout: DEPLOY_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
await admin.query('SELECT pg_advisory_lock($1)', [HARNESS_DEPLOY_LOCK_KEY]);
|
||||
try {
|
||||
await execFileAsync(
|
||||
process.execPath,
|
||||
[
|
||||
PRISMA_CLI,
|
||||
'migrate',
|
||||
'deploy',
|
||||
'--config',
|
||||
path.join(workDir, 'prisma.config.ts'),
|
||||
],
|
||||
{
|
||||
cwd: BACKEND_ROOT,
|
||||
env: { ...process.env, MIGRATION_HARNESS_URL: scratchUrl },
|
||||
timeout: DEPLOY_TIMEOUT_MS,
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
await admin.query('SELECT pg_advisory_unlock($1)', [
|
||||
HARNESS_DEPLOY_LOCK_KEY,
|
||||
]);
|
||||
}
|
||||
};
|
||||
|
||||
let client: Client | null = null;
|
||||
|
||||
Reference in new issue
Block a user