Skip to content

Commit d75a4ac

Browse files
Fallback to new Reaper when reused one is unreachable (#1233)
1 parent 84d0908 commit d75a4ac

File tree

2 files changed

+45
-13
lines changed

2 files changed

+45
-13
lines changed

packages/testcontainers/src/reaper/reaper.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,24 @@ describe.sequential("Reaper", { timeout: 120_000 }, () => {
5353
expect(reaper2.containerId).toBe(reaper.containerId);
5454
});
5555

56+
it("should create new reaper container when existing reaper cannot be reached", async () => {
57+
const reaper = await getReaper();
58+
vi.resetModules();
59+
const unreachablePort = await new RandomPortGenerator().generatePort();
60+
const reaperContainerInfo = (await client.container.list()).filter((c) => c.Id === reaper.containerId)[0];
61+
reaperContainerInfo.Labels["TESTCONTAINERS_RYUK_TEST_LABEL"] = "false";
62+
const reaperPort = reaperContainerInfo.Ports.find((port) => port.PrivatePort == 8080);
63+
if (!reaperPort) {
64+
throw new Error("Expected Reaper to map exposed port 8080");
65+
}
66+
reaperPort.PublicPort = unreachablePort;
67+
vi.spyOn(client.container, "list").mockResolvedValue([reaperContainerInfo]);
68+
69+
const reaper2 = await getReaper();
70+
71+
expect(reaper2.containerId).not.toBe(reaper.containerId);
72+
});
73+
5674
it("should use custom port when TESTCONTAINERS_RYUK_PORT is set", async () => {
5775
const customPort = (await new RandomPortGenerator().generatePort()).toString();
5876
vi.stubEnv("TESTCONTAINERS_RYUK_PORT", customPort);

packages/testcontainers/src/reaper/reaper.ts

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,44 @@ export async function getReaper(client: ContainerRuntimeClient): Promise<Reaper>
2929
}
3030

3131
reaper = await withFileLock("testcontainers-node.lock", async () => {
32-
const reaperContainer = await findReaperContainer(client);
33-
sessionId = reaperContainer?.Labels[LABEL_TESTCONTAINERS_SESSION_ID] ?? new RandomUuid().nextUuid();
32+
const reaperContainers = await findReaperContainers(client);
3433

3534
if (process.env.TESTCONTAINERS_RYUK_DISABLED === "true") {
35+
sessionId = new RandomUuid().nextUuid();
3636
return new DisabledReaper(sessionId, "");
37-
} else if (reaperContainer) {
38-
return await useExistingReaper(reaperContainer, sessionId, client.info.containerRuntime.host);
39-
} else {
40-
return await createNewReaper(sessionId, client.info.containerRuntime.remoteSocketPath);
4137
}
38+
39+
for (const reaperContainer of reaperContainers) {
40+
const existingSessionId = reaperContainer.Labels[LABEL_TESTCONTAINERS_SESSION_ID] ?? new RandomUuid().nextUuid();
41+
try {
42+
sessionId = existingSessionId;
43+
return await useExistingReaper(reaperContainer, sessionId, client.info.containerRuntime.host);
44+
} catch (error) {
45+
const message = error instanceof Error ? error.message : String(error);
46+
log.warn(`Failed to reuse existing Reaper: ${message}. Trying another Reaper...`, {
47+
containerId: reaperContainer.Id,
48+
});
49+
}
50+
}
51+
52+
sessionId = new RandomUuid().nextUuid();
53+
return await createNewReaper(sessionId, client.info.containerRuntime.remoteSocketPath);
4254
});
4355

4456
reaper.addSession(sessionId);
4557
return reaper;
4658
}
4759

48-
async function findReaperContainer(client: ContainerRuntimeClient): Promise<ContainerInfo | undefined> {
60+
async function findReaperContainers(client: ContainerRuntimeClient): Promise<ContainerInfo[]> {
4961
const containers = await client.container.list();
50-
return containers.find(
51-
(container) =>
52-
container.State === "running" &&
53-
container.Labels[LABEL_TESTCONTAINERS_RYUK] === "true" &&
54-
container.Labels["TESTCONTAINERS_RYUK_TEST_LABEL"] !== "true"
55-
);
62+
return containers
63+
.filter(
64+
(container) =>
65+
container.State === "running" &&
66+
container.Labels[LABEL_TESTCONTAINERS_RYUK] === "true" &&
67+
container.Labels["TESTCONTAINERS_RYUK_TEST_LABEL"] !== "true"
68+
)
69+
.sort((a, b) => b.Created - a.Created);
5670
}
5771

5872
async function useExistingReaper(reaperContainer: ContainerInfo, sessionId: string, host: string): Promise<Reaper> {

0 commit comments

Comments
 (0)