Skip to content

Commit ed97cee

Browse files
ericallamTrigger.dev RepoOps
authored andcommitted
perf(testcontainers): speed up Postgres-backed test fixtures
Speeds up the Postgres-backed test fixtures. - One Postgres container per image is now shared by every test file and test process (testcontainers reuse). The schema lives in a template database named by its hash and built once under an advisory lock; each test still gets its own cloned database. Set `TESTCONTAINERS_REUSE_ENABLE=false` to get a private container. - Test Postgres runs with `fsync` and `full_page_writes` off on tmpfs. On PG14 every `CREATE DATABASE ... TEMPLATE` forces a checkpoint, so each per-test clone drops from seconds to about 100ms. - The schema is applied as one SQL script from `prisma migrate diff --from-empty`, cached on disk by schema and Prisma version, in place of `prisma db push`. - `postgresAndRedisTest` and `replicationContainerTest` clone a database per test from a Postgres private to the test file (logical replication slots are cluster-wide and tests name them), and drop the clone's slots before dropping it. The e2e servers (`startTestServer`, `startSessionStreamTestServer`) clone theirs from the shared Postgres. - `containerTest` and `containerTestWithIsolatedRedis` no longer boot ClickHouse. Tests that read it use the new `containerTestWithClickhouse`. `containerTestWithIsolatedRedisNoClickhouse` is folded into `containerTestWithIsolatedRedis`. Mono-RevId: d3e80a3518642770f0edfb15726b08a36e55686e
1 parent 1616cda commit ed97cee

24 files changed

Lines changed: 726 additions & 613 deletions

‎apps/webapp/test/apiRunListPresenter.test.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ vi.mock("~/db.server", async () => {
4646
});
4747

4848
import { ClickHouse } from "@internal/clickhouse";
49-
import { containerTest } from "@internal/testcontainers";
49+
import { containerTestWithClickhouse } from "@internal/testcontainers";
5050
import { CURRENT_API_VERSION } from "~/api/versions";
5151
import { ApiRunListPresenter } from "~/presenters/v3/ApiRunListPresenter.server";
5252
import {
@@ -71,7 +71,7 @@ function setupClients(prisma: unknown, clickhouseUrl: string): ClickHouse {
7171
}
7272

7373
describe("ApiRunListPresenter public /runs list", () => {
74-
containerTest(
74+
containerTestWithClickhouse(
7575
"genuinely-empty env returns { data: [], pagination } without error",
7676
async ({ clickhouseContainer, prisma }) => {
7777
setupClients(prisma, clickhouseContainer.getConnectionUrl());
@@ -97,7 +97,7 @@ describe("ApiRunListPresenter public /runs list", () => {
9797
}
9898
);
9999

100-
containerTest(
100+
containerTestWithClickhouse(
101101
"env scoping resolves via the control-plane _replica handle (filter[env], 4th arg omitted)",
102102
async ({ clickhouseContainer, prisma }) => {
103103
const clickhouse = setupClients(prisma, clickhouseContainer.getConnectionUrl());
@@ -128,7 +128,7 @@ describe("ApiRunListPresenter public /runs list", () => {
128128
}
129129
);
130130

131-
containerTest(
131+
containerTestWithClickhouse(
132132
"single-DB passthrough: no readThroughDeps lists the seeded runs unchanged",
133133
async ({ clickhouseContainer, prisma }) => {
134134
const clickhouse = setupClients(prisma, clickhouseContainer.getConnectionUrl());

‎apps/webapp/test/clickhouseQueryMetrics.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ClickHouse } from "@internal/clickhouse";
2-
import { containerTest } from "@internal/testcontainers";
2+
import { containerTestWithClickhouse } from "@internal/testcontainers";
33
import { describe, expect, vi } from "vitest";
44
import { RunsRepository } from "~/services/runsRepository/runsRepository.server";
55
import {
@@ -15,7 +15,7 @@ vi.mock("~/db.server", () => ({ prisma: {}, $replica: {} }));
1515
vi.setConfig({ testTimeout: 90_000 });
1616

1717
describe("clickhouse query metrics", () => {
18-
containerTest(
18+
containerTestWithClickhouse(
1919
"records duration + read_rows on success and an error metric with the ClickHouse error type",
2020
async ({ clickhouseContainer, prisma }) => {
2121
const ctx = await seedParents(prisma, "chm");

‎apps/webapp/test/dashboardAgentRunTrace.test.ts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// real Postgres and ClickHouse behind them.
55
import { ClickHouse, type TaskEventV2Input } from "@internal/clickhouse";
66
import { setupAuthenticatedEnvironment } from "@internal/run-engine/tests";
7-
import { containerTest } from "@internal/testcontainers";
7+
import { containerTestWithClickhouse } from "@internal/testcontainers";
88
import type { PrismaClient } from "@trigger.dev/database";
99
import { describe, expect, vi } from "vitest";
1010
import {
@@ -413,7 +413,7 @@ function depthOf(rootSpan: DashboardAgentTraceSpan | undefined): number {
413413
}
414414

415415
describe("the dashboard agent run trace", () => {
416-
containerTest(
416+
containerTestWithClickhouse(
417417
"reports the same millisecond durations, task slugs and customer-visible events on both event stores",
418418
async ({ prisma, clickhouseContainer }) => {
419419
const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
@@ -506,7 +506,7 @@ describe("the dashboard agent run trace", () => {
506506
TIMEOUT_MS
507507
);
508508

509-
containerTest(
509+
containerTestWithClickhouse(
510510
"keeps a chain well under the depth guard fully intact and JSON-serializable",
511511
async ({ prisma }) => {
512512
const { repository, environmentId, organizationId, startedAt } =
@@ -529,7 +529,7 @@ describe("the dashboard agent run trace", () => {
529529
TIMEOUT_MS
530530
);
531531

532-
containerTest(
532+
containerTestWithClickhouse(
533533
"does not truncate a chain of exactly the depth guard",
534534
async ({ prisma }) => {
535535
const { repository, environmentId, organizationId, startedAt } =
@@ -557,7 +557,7 @@ describe("the dashboard agent run trace", () => {
557557
TIMEOUT_MS
558558
);
559559

560-
containerTest(
560+
containerTestWithClickhouse(
561561
"truncates a chain deeper than the depth guard instead of throwing",
562562
async ({ prisma }) => {
563563
const { repository, environmentId, organizationId, startedAt } =
@@ -586,7 +586,7 @@ describe("the dashboard agent run trace", () => {
586586
},
587587
TIMEOUT_MS
588588
);
589-
containerTest(
589+
containerTestWithClickhouse(
590590
"builds a child run's trace when the trace root is outside the child's window",
591591
async ({ prisma, clickhouseContainer }) => {
592592
const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");

‎apps/webapp/test/engine/streamBatchItems.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { PostgresRunStore } from "@internal/run-store";
1919
import { setupAuthenticatedEnvironment } from "@internal/run-engine/tests";
2020
// Per-test redis isolation: each test runs its own RunEngine whose background work outlives the test
2121
// body. NoClickhouse because this suite never touches ClickHouse - skips the worker-scoped boot+migrate.
22-
import { containerTestWithIsolatedRedisNoClickhouse as containerTest } from "@internal/testcontainers";
22+
import { containerTestWithIsolatedRedis as containerTest } from "@internal/testcontainers";
2323
import { trace } from "@opentelemetry/api";
2424
import type { PrismaClient } from "@trigger.dev/database";
2525
import { BatchId } from "@trigger.dev/core/v3/isomorphic";

‎apps/webapp/test/locateAgentObject.test.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import type { LocateResult } from "@internal/dashboard-agent-contracts";
88
import type { PrismaClient } from "@trigger.dev/database";
99
import { ClickHouse } from "@internal/clickhouse";
1010
import { RunId } from "@trigger.dev/core/v3/isomorphic";
11-
import { containerTest } from "@internal/testcontainers";
11+
import { containerTestWithClickhouse } from "@internal/testcontainers";
1212
import { setTimeout as sleep } from "node:timers/promises";
1313
import { describe, expect, vi } from "vitest";
1414
import {
@@ -202,7 +202,7 @@ function locateTest(
202202
name: string,
203203
fn: (ctx: { prisma: PrismaClient; w: AgentWorld }) => Promise<void>
204204
) {
205-
containerTest(name, async ({ prisma }) => {
205+
containerTestWithClickhouse(name, async ({ prisma }) => {
206206
db.client = prisma;
207207
await fn({ prisma, w: await seedAgentWorld(prisma) });
208208
});
@@ -217,7 +217,7 @@ type ErrorCtx = {
217217

218218
/** Same, plus a real `ClickHouse` client wired into the mocked factory for the error kind. */
219219
function errorLocateTest(name: string, fn: (ctx: ErrorCtx) => Promise<void>) {
220-
containerTest(name, async ({ prisma, clickhouseContainer, clickhouseClient }) => {
220+
containerTestWithClickhouse(name, async ({ prisma, clickhouseContainer, clickhouseClient }) => {
221221
db.client = prisma;
222222
const clickhouse = new ClickHouse({ url: clickhouseContainer.getConnectionUrl() });
223223
chFactory.getClickhouseForOrganization.mockResolvedValue(clickhouse);

‎apps/webapp/test/queueArchive.test.ts‎

Lines changed: 60 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ vi.mock("~/services/clickhouse/clickhouseFactoryInstance.server", () => ({
2323
import { ClickHouse } from "@internal/clickhouse";
2424
import { RunEngine } from "@internal/run-engine";
2525
import { setupAuthenticatedEnvironment, setupBackgroundWorker } from "@internal/run-engine/tests";
26-
import { containerTest } from "@internal/testcontainers";
26+
import { containerTestWithClickhouse } from "@internal/testcontainers";
2727
import { trace } from "@opentelemetry/api";
2828
import { generateFriendlyId } from "@trigger.dev/core/v3/isomorphic";
2929
import type { PrismaClient } from "@trigger.dev/database";
@@ -112,7 +112,7 @@ async function triggerOnQueue(
112112
}
113113

114114
describe("QueueListPresenter archived filter", () => {
115-
containerTest(
115+
containerTestWithClickhouse(
116116
"exclude hides archived queues across list, search and counts; include keeps them",
117117
async ({ prisma, redisOptions }) => {
118118
const engine = buildEngine(prisma, redisOptions);
@@ -159,7 +159,7 @@ describe("QueueListPresenter archived filter", () => {
159159
}
160160
);
161161

162-
containerTest(
162+
containerTestWithClickhouse(
163163
"returns archived queues with queued runs as active, and skips idle archived queues",
164164
async ({ prisma, redisOptions }) => {
165165
const engine = buildEngine(prisma, redisOptions);
@@ -209,7 +209,7 @@ describe("QueueListPresenter archived filter", () => {
209209
});
210210

211211
describe("public queue shape", () => {
212-
containerTest(
212+
containerTestWithClickhouse(
213213
"the public API item never exposes archive state",
214214
async ({ prisma, redisOptions }) => {
215215
const engine = buildEngine(prisma, redisOptions);
@@ -229,32 +229,35 @@ describe("public queue shape", () => {
229229
});
230230

231231
describe("QueueAllocationPresenter", () => {
232-
containerTest("excludes archived queues from the allocated sum", async ({ prisma }) => {
233-
const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
234-
await createQueue(prisma, environment, "emails", { concurrencyLimit: 20 });
235-
await createQueue(prisma, environment, "images", { concurrencyLimit: 30 });
236-
await createQueue(prisma, environment, "old", {
237-
concurrencyLimit: 25,
238-
archivedAt: new Date(),
239-
});
240-
241-
const allocation = await new QueueAllocationPresenter(prisma, prisma).call({
242-
environment: { ...environment, maximumConcurrencyLimit: 100 },
243-
});
244-
245-
expect(allocation).toEqual({ totalQueues: 2, allocated: 50, unlimitedCount: 0 });
246-
247-
// With archiving disabled for the org, the tile counts every queue, like the list.
248-
const unfiltered = await new QueueAllocationPresenter(prisma, prisma).call({
249-
environment: { ...environment, maximumConcurrencyLimit: 100 },
250-
excludeArchived: false,
251-
});
252-
expect(unfiltered).toEqual({ totalQueues: 3, allocated: 75, unlimitedCount: 0 });
253-
});
232+
containerTestWithClickhouse(
233+
"excludes archived queues from the allocated sum",
234+
async ({ prisma }) => {
235+
const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
236+
await createQueue(prisma, environment, "emails", { concurrencyLimit: 20 });
237+
await createQueue(prisma, environment, "images", { concurrencyLimit: 30 });
238+
await createQueue(prisma, environment, "old", {
239+
concurrencyLimit: 25,
240+
archivedAt: new Date(),
241+
});
242+
243+
const allocation = await new QueueAllocationPresenter(prisma, prisma).call({
244+
environment: { ...environment, maximumConcurrencyLimit: 100 },
245+
});
246+
247+
expect(allocation).toEqual({ totalQueues: 2, allocated: 50, unlimitedCount: 0 });
248+
249+
// With archiving disabled for the org, the tile counts every queue, like the list.
250+
const unfiltered = await new QueueAllocationPresenter(prisma, prisma).call({
251+
environment: { ...environment, maximumConcurrencyLimit: 100 },
252+
excludeArchived: false,
253+
});
254+
expect(unfiltered).toEqual({ totalQueues: 3, allocated: 75, unlimitedCount: 0 });
255+
}
256+
);
254257
});
255258

256259
describe("ArchiveQueueService", () => {
257-
containerTest(
260+
containerTestWithClickhouse(
258261
"archives an idle queue without touching limits, overrides, paused state or worker links",
259262
async ({ prisma, redisOptions }) => {
260263
const engine = buildEngine(prisma, redisOptions);
@@ -296,28 +299,31 @@ describe("ArchiveQueueService", () => {
296299
}
297300
);
298301

299-
containerTest("refuses to archive a queue with queued runs", async ({ prisma, redisOptions }) => {
300-
const engine = buildEngine(prisma, redisOptions);
301-
onTestFinished(() => engine.quit());
302-
const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
303-
await setupBackgroundWorker(engine, environment, "busy-task");
304-
await deployWithout(engine, environment);
305-
const queue = await prisma.taskQueue.findFirstOrThrow({
306-
where: { runtimeEnvironmentId: environment.id, name: "task/busy-task" },
307-
});
308-
await triggerOnQueue(engine, prisma, environment, "busy-task", queue.name);
309-
310-
const result = await new ArchiveQueueService(prisma, engine).archive(
311-
environment,
312-
queue.friendlyId
313-
);
314-
expect(result.isErr() && result.error.type).toBe("queue_has_active_runs");
302+
containerTestWithClickhouse(
303+
"refuses to archive a queue with queued runs",
304+
async ({ prisma, redisOptions }) => {
305+
const engine = buildEngine(prisma, redisOptions);
306+
onTestFinished(() => engine.quit());
307+
const environment = await setupAuthenticatedEnvironment(prisma, "PRODUCTION");
308+
await setupBackgroundWorker(engine, environment, "busy-task");
309+
await deployWithout(engine, environment);
310+
const queue = await prisma.taskQueue.findFirstOrThrow({
311+
where: { runtimeEnvironmentId: environment.id, name: "task/busy-task" },
312+
});
313+
await triggerOnQueue(engine, prisma, environment, "busy-task", queue.name);
315314

316-
const after = await prisma.taskQueue.findFirstOrThrow({ where: { id: queue.id } });
317-
expect(after.archivedAt).toBeNull();
318-
});
315+
const result = await new ArchiveQueueService(prisma, engine).archive(
316+
environment,
317+
queue.friendlyId
318+
);
319+
expect(result.isErr() && result.error.type).toBe("queue_has_active_runs");
320+
321+
const after = await prisma.taskQueue.findFirstOrThrow({ where: { id: queue.id } });
322+
expect(after.archivedAt).toBeNull();
323+
}
324+
);
319325

320-
containerTest(
326+
containerTestWithClickhouse(
321327
"counts runs handed to a worker queue but not yet picked up as active",
322328
async ({ prisma, redisOptions }) => {
323329
const engine = buildEngine(prisma, redisOptions);
@@ -364,7 +370,7 @@ describe("ArchiveQueueService", () => {
364370
}
365371
);
366372

367-
containerTest(
373+
containerTestWithClickhouse(
368374
"refuses to archive paused queues, limit-0 or combined-limit-0 queues and named concurrency limits",
369375
async ({ prisma, redisOptions }) => {
370376
const engine = buildEngine(prisma, redisOptions);
@@ -404,7 +410,7 @@ describe("ArchiveQueueService", () => {
404410
}
405411
);
406412

407-
containerTest(
413+
containerTestWithClickhouse(
408414
"check reports the reason and run count without archiving",
409415
async ({ prisma, redisOptions }) => {
410416
const engine = buildEngine(prisma, redisOptions);
@@ -430,7 +436,7 @@ describe("ArchiveQueueService", () => {
430436
}
431437
);
432438

433-
containerTest(
439+
containerTestWithClickhouse(
434440
"refuses to archive a queue the current or an unpromoted deploy declares",
435441
async ({ prisma, redisOptions }) => {
436442
const engine = buildEngine(prisma, redisOptions);
@@ -470,7 +476,7 @@ describe("ArchiveQueueService", () => {
470476
}
471477
);
472478

473-
containerTest(
479+
containerTestWithClickhouse(
474480
"a deploy created in the same millisecond as the current one still blocks archiving",
475481
async ({ prisma, redisOptions }) => {
476482
const engine = buildEngine(prisma, redisOptions);
@@ -506,7 +512,7 @@ describe("ArchiveQueueService", () => {
506512
}
507513
);
508514

509-
containerTest(
515+
containerTestWithClickhouse(
510516
"a queue update landing between the check and the write aborts the archive",
511517
async ({ prisma, redisOptions }) => {
512518
const engine = buildEngine(prisma, redisOptions);
@@ -538,7 +544,7 @@ describe("ArchiveQueueService", () => {
538544
}
539545
);
540546

541-
containerTest(
547+
containerTestWithClickhouse(
542548
"unarchive brings the queue back to the default list",
543549
async ({ prisma, redisOptions }) => {
544550
const engine = buildEngine(prisma, redisOptions);
@@ -565,7 +571,7 @@ describe("ArchiveQueueService", () => {
565571
});
566572

567573
describe("QueueListPresenter ranked sort", () => {
568-
containerTest(
574+
containerTestWithClickhouse(
569575
"a busy archived queue doesn't take a slot or push queues off the last page",
570576
async ({ prisma, redisOptions, clickhouseContainer }) => {
571577
const engine = buildEngine(prisma, redisOptions);

‎apps/webapp/test/runEngineBatchTriggerStoreRouting.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ vi.mock("~/db.server", () => ({
1717
import { RunEngine } from "@internal/run-engine";
1818
import { setupAuthenticatedEnvironment } from "@internal/run-engine/tests";
1919
import { PostgresRunStore, RoutingRunStore } from "@internal/run-store";
20-
import { containerTestWithIsolatedRedisNoClickhouse as containerTest } from "@internal/testcontainers";
20+
import { containerTestWithIsolatedRedis as containerTest } from "@internal/testcontainers";
2121
import { trace } from "@opentelemetry/api";
2222
import { BatchId } from "@trigger.dev/core/v3/isomorphic";
2323
import type { PrismaClient } from "@trigger.dev/database";

‎apps/webapp/test/runsListQueryError.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ClickHouse } from "@internal/clickhouse";
2-
import { containerTest } from "@internal/testcontainers";
2+
import { containerTestWithClickhouse } from "@internal/testcontainers";
33
import { describe, expect, vi } from "vitest";
44
import {
55
RunsListQueryError,
@@ -16,7 +16,7 @@ vi.mock("~/db.server", () => ({ prisma: {}, $replica: {} }));
1616
vi.setConfig({ testTimeout: 90_000 });
1717

1818
describe("runs list query error handling", () => {
19-
containerTest(
19+
containerTestWithClickhouse(
2020
"a ClickHouse resource-limit error surfaces as RunsListQueryError",
2121
async ({ clickhouseContainer, prisma }) => {
2222
const ctx = await seedParents(prisma, "qerr");

0 commit comments

Comments
 (0)