mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
Merge branch 'release/v1.45.0' into feat/v145-deps
This commit is contained in:
commit
5a9a6ce78b
4 files changed
+80
-14
No files matched your search
@@ -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);
|
||||
});
|
||||
@@ -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(
|
||||
|
||||
Reference in new issue
Block a user