Skip to content

Commit c536b9c

Browse files
committed
test(testcontainers): add opt-in CI-resource-limit mode for local flake repro
1 parent f9b5c57 commit c536b9c

3 files changed

Lines changed: 51 additions & 9 deletions

File tree

internal-packages/testcontainers/TESTING.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,23 @@ Container lifecycle (boot + migrate + teardown) dominates these suites. To see t
3434
TESTCONTAINERS_TIMING=1 pnpm exec vitest run <file> --disableConsoleIntercept
3535
```
3636

37+
## Approximating the 2-core CI runner locally (flake repro)
38+
39+
To reproduce CI-like CPU pressure on a beefy local machine - useful when a test only flakes under
40+
the 2-core CI runner:
41+
42+
```bash
43+
# cap each testcontainer's CPU/mem (TESTCONTAINERS_CPU = cores, TESTCONTAINERS_MEMORY_GB = GB),
44+
# and pin the test runner to 2 cores. Off unless the env vars are set.
45+
TESTCONTAINERS_CPU=2 TESTCONTAINERS_MEMORY_GB=2 taskset -c 0,1 pnpm exec vitest run <file>
46+
```
47+
48+
Note: in practice the scoped tests here are latency/IO/sleep-bound, not CPU-bound, so this changes
49+
timings little - the original CI slowness was per-test container *boots*, which worker-scoping removed.
50+
Keep it for the cases that genuinely starve on CPU (e.g. timing races against a worker poll).
51+
52+
## Timing harness
53+
3754
Or use the harness, which aggregates the split for you:
3855

3956
```bash

internal-packages/testcontainers/src/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
postgresUriWithDatabase,
1313
pushDatabaseSchema,
1414
useContainer,
15+
withCiResourceLimits,
1516
withContainerSetup,
1617
} from "./utils";
1718
import { getTaskMetadata, logCleanup, logSetup } from "./logs";
@@ -147,7 +148,9 @@ let workerPostgresContainer: Promise<StartedPostgreSqlContainer> | undefined;
147148
const getWorkerPostgresContainer = () => {
148149
if (!workerPostgresContainer) {
149150
workerPostgresContainer = (async () => {
150-
const container = await new PostgreSqlContainer("docker.io/postgres:14")
151+
const container = await withCiResourceLimits(
152+
new PostgreSqlContainer("docker.io/postgres:14")
153+
)
151154
.withCommand(["-c", "listen_addresses=*", "-c", "wal_level=logical"])
152155
.start();
153156
await pushDatabaseSchema(
@@ -358,7 +361,7 @@ type ClickhouseTestContext = {
358361

359362
// Boot + migrate clickhouse once per worker.
360363
const bootWorkerClickhouse = async ({}, use: Use<StartedClickHouseContainer>) => {
361-
const container = await new ClickHouseContainer().start();
364+
const container = await withCiResourceLimits(new ClickHouseContainer()).start();
362365
const client = createClient({ url: container.getConnectionUrl() });
363366
await client.ping();
364367
await runClickhouseMigrations(client, clickhouseMigrationsPath);
@@ -469,7 +472,7 @@ export const containerWithElectricAndRedisTest = test.extend<ContainerWithElectr
469472

470473
// Boot minio once per worker; reset the bucket per test (auto fixture).
471474
const bootWorkerMinio = async ({}, use: Use<StartedMinIOContainer>) => {
472-
const container = await new MinIOContainer().start();
475+
const container = await withCiResourceLimits(new MinIOContainer()).start();
473476
try {
474477
await use(container);
475478
} finally {

internal-packages/testcontainers/src/utils.ts

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,26 @@ export async function pushDatabaseSchema(databaseUrl: string) {
4646
);
4747
}
4848

49+
/**
50+
* Caps each container's CPU/memory to approximate the 2-core CI runner locally (for timing + flake
51+
* reproduction). Set TESTCONTAINERS_CPU (cores per container, e.g. "2") and/or
52+
* TESTCONTAINERS_MEMORY_GB (GB per container). Pair with running the runner under `taskset -c 0,1`.
53+
* No-op when neither is set. (testcontainers v11 has no cpuset pinning, only this quota cap.)
54+
*/
55+
export function withCiResourceLimits<T extends GenericContainer>(container: T): T {
56+
const cpu = process.env.TESTCONTAINERS_CPU;
57+
const memory = process.env.TESTCONTAINERS_MEMORY_GB;
58+
if (!cpu && !memory) {
59+
return container;
60+
}
61+
return container.withResourcesQuota({
62+
...(cpu ? { cpu: Number(cpu) } : {}),
63+
...(memory ? { memory: Number(memory) } : {}),
64+
});
65+
}
66+
4967
export async function createPostgresContainer(network: StartedNetwork) {
50-
const container = await new PostgreSqlContainer("docker.io/postgres:14")
68+
const container = await withCiResourceLimits(new PostgreSqlContainer("docker.io/postgres:14"))
5169
.withNetwork(network)
5270
.withNetworkAliases("database")
5371
.withCommand(["-c", "listen_addresses=*", "-c", "wal_level=logical"])
@@ -59,7 +77,9 @@ export async function createPostgresContainer(network: StartedNetwork) {
5977
}
6078

6179
export async function createClickHouseContainer(network: StartedNetwork) {
62-
const container = await new ClickHouseContainer().withNetwork(network).start();
80+
const container = await withCiResourceLimits(new ClickHouseContainer())
81+
.withNetwork(network)
82+
.start();
6383

6484
const client = createClient({
6585
url: container.getConnectionUrl(),
@@ -86,7 +106,7 @@ export async function createRedisContainer({
86106
port?: number;
87107
network?: StartedNetwork;
88108
}) {
89-
let container = new RedisContainer("redis:7.2")
109+
let container = withCiResourceLimits(new RedisContainer("redis:7.2"))
90110
.withExposedPorts(port ?? 6379)
91111
.withStartupTimeout(120_000); // 2 minutes
92112

@@ -167,8 +187,10 @@ export async function createElectricContainer(
167187
network.getName()
168188
)}:5432/${postgresContainer.getDatabase()}?sslmode=disable`;
169189

170-
const container = await new GenericContainer(
171-
"electricsql/electric:1.2.4@sha256:20da3d0b0e74926c5623392db67fd56698b9e374c4aeb6cb5cadeb8fea171c36"
190+
const container = await withCiResourceLimits(
191+
new GenericContainer(
192+
"electricsql/electric:1.2.4@sha256:20da3d0b0e74926c5623392db67fd56698b9e374c4aeb6cb5cadeb8fea171c36"
193+
)
172194
)
173195
.withExposedPorts(3000)
174196
.withNetwork(network)
@@ -185,7 +207,7 @@ export async function createElectricContainer(
185207
}
186208

187209
export async function createMinIOContainer(network: StartedNetwork) {
188-
const container = await new MinIOContainer()
210+
const container = await withCiResourceLimits(new MinIOContainer())
189211
.withNetwork(network)
190212
.withNetworkAliases("minio")
191213
.start();

0 commit comments

Comments
 (0)