Merge branch 'release/v1.45.0' into feat/v145-deps

This commit is contained in:
Marc-André Bombeck committed 2026-10-22 07:54:27 +02:00
commit 5a9a6ce78b
4 files changed
+80 -14

No files matched your search

+3 -1
View File
@@ -121,7 +121,9 @@ Each Vitest worker clones it on its first file (`CREATE DATABASE … TEMPLATE`,
see `tests/integration/worker-database.ts`), so files run in parallel without
seeing each other's rows, advisory locks or queue schema. The worker count
follows the host (half the cores, at most eight) unless
`HEALTHLOG_INTEGRATION_WORKERS` pins it; CI uses three.
`HEALTHLOG_INTEGRATION_WORKERS` pins it; CI uses three. To reproduce a CI runner's
contention on a larger machine, cap the database container with
`HEALTHLOG_INTEGRATION_PG_CPUS=2`.
`truncateAllTables` empties every table the migrations created and puts back
the rows they seed (the mood-tag and symptom catalogues), so a test starts
@@ -335,5 +335,12 @@ describe("dashboard all-time counts — parity with the live count", () => {
expect(legacy.get("PULSE")).toBeGreaterThan(500_000);
expect(legacy.get("WEIGHT")).toBe(5);
expect(legacy.get("BLOOD_GLUCOSE")).toBe(2);
});
// Over half a million rows written and folded into DAY buckets: about
// 35 s on a CI runner when the file ran alone. With three files sharing
// the runner's Postgres it took 1.7 times as long (measured with the
// database capped at two CPUs, HEALTHLOG_INTEGRATION_PG_CPUS=2: 14.6 s
// alone, 25.3 s beside the sharing matrix and the restore memory test),
// past the suite's 60 s default. The work is the point of the case, so
// the budget is sized to it: twice the contended estimate.
}, 150_000);
});
+16 -8
View File
@@ -44,7 +44,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const PROJECT_ROOT = resolve(__dirname, "..", "..");
let container: StartedPostgreSqlContainer | null = null;
let started: StartedPostgreSqlContainer | null = null;
declare module "vitest" {
export interface ProvidedContext {
@@ -56,7 +56,7 @@ declare module "vitest" {
export default async function setup(
project: TestProject,
): Promise<() => Promise<void>> {
container = await new PostgreSqlContainer("postgres:16-alpine")
let container = new PostgreSqlContainer("postgres:16-alpine")
.withDatabase(TEMPLATE_DATABASE)
.withUsername("healthlog")
.withPassword("healthlog")
@@ -74,10 +74,18 @@ export default async function setup(
"full_page_writes=off",
"-c",
"max_connections=400",
])
.start();
]);
// A CPU quota for the database container, to reproduce a CI runner's
// contention on a larger machine: HEALTHLOG_INTEGRATION_PG_CPUS=2.
const cpus = Number.parseFloat(
process.env.HEALTHLOG_INTEGRATION_PG_CPUS ?? "",
);
if (Number.isFinite(cpus) && cpus > 0) {
container = container.withResourcesQuota({ cpu: cpus });
}
started = await container.start();
const url = container.getConnectionUri() + "?schema=public&pgbouncer=false";
const url = started.getConnectionUri() + "?schema=public&pgbouncer=false";
process.env.DATABASE_URL = url;
// The explicit child environment keeps Prisma CLI on the same container.
@@ -91,9 +99,9 @@ export default async function setup(
project.provide("integrationDatabasePlan", await readTemplate(url));
return async () => {
if (container) {
await container.stop({ remove: true });
container = null;
if (started) {
await started.stop({ remove: true });
started = null;
}
};
}
@@ -145,6 +145,47 @@ async function holdLock(): Promise<Client> {
return holder;
}
/** Sessions of this database queued behind a lock, other than the holder. */
async function lockWaiters(db: Client, holderPid: number): Promise<number> {
const { rows } = await db.query<{ n: string }>(
`SELECT count(*) AS n FROM pg_stat_activity
WHERE datname = current_database()
AND wait_event_type = 'Lock'
AND pid <> $1`,
[holderPid],
);
return Number(rows[0]?.n ?? 0);
}
/**
* Let go of the held lock once a migration attempt has queued behind it and
* given up. Resolves false when no attempt queued within the budget, so the
* test reports that instead of passing on a run that never met the lock.
*/
async function releaseAfterFirstTimeout(holder: Client): Promise<boolean> {
const { rows } = await holder.query<{ pid: number }>(
"SELECT pg_backend_pid() AS pid",
);
const holderPid = rows[0].pid;
const watcher = await connect();
const until = async (predicate: (n: number) => boolean) => {
const deadline = Date.now() + 60_000;
while (Date.now() < deadline) {
if (predicate(await lockWaiters(watcher, holderPid))) return true;
await new Promise((resolve) => setTimeout(resolve, 25));
}
return false;
};
try {
const queued = await until((n) => n > 0);
if (queued) await until((n) => n === 0);
await holder.query("ROLLBACK");
return queued;
} finally {
await watcher.end();
}
}
async function state(db: Client) {
const rows = await db.query(
`SELECT migration_name, finished_at IS NOT NULL AS finished,
@@ -181,15 +222,23 @@ describe("migration 0412 under a held lock", () => {
add0412(dir, { marked: true });
const holder = await holdLock();
// The first attempt meets the lock; the holder lets go during the backoff.
const release = setTimeout(() => {
void holder.query("ROLLBACK");
}, 2_500);
// The release follows the migration rather than a clock: it waits until
// the first attempt is seen queued behind the lock and then gone again
// (its one-second timeout fired), and lets go before the retry. A fixed
// delay raced the CLI's own start-up, which on a loaded runner outlasted
// the delay, so the first attempt found the lock already free.
const released = releaseAfterFirstTimeout(holder);
const startedAt = Date.now();
const result = await migrateDeploy(dir);
clearTimeout(release);
const releasedInTime = await released;
await holder.query("ROLLBACK").catch(() => {});
await holder.end();
expect(
releasedInTime,
"the first attempt never queued behind the held lock",
).toBe(true);
expect(result.code, result.stderr).toBe(0);
expect(result.stderr).toContain("Database error code: 55P03");
expect(result.stdout).toContain(