From 510969cec645e3005a7620409873c3c9e3ed8f61 Mon Sep 17 00:00:00 2001 From: Gilles De Waele Date: Mon, 6 Jan 2025 14:29:09 +0100 Subject: [PATCH] feat(file-lock): Added configurable retries for file-lock --- packages/testcontainers/package.json | 1 + .../testcontainers/src/common/file-lock.ts | 9 ++++-- packages/testcontainers/src/common/index.ts | 1 + .../testcontainers/src/common/preferences.ts | 11 +++++++ packages/testcontainers/src/reaper/reaper.ts | 30 +++++++++++-------- .../testcontainers/src/test-containers.ts | 5 ++++ 6 files changed, 42 insertions(+), 15 deletions(-) create mode 100644 packages/testcontainers/src/common/preferences.ts diff --git a/packages/testcontainers/package.json b/packages/testcontainers/package.json index 27cd13c17..2d279916b 100644 --- a/packages/testcontainers/package.json +++ b/packages/testcontainers/package.json @@ -32,6 +32,7 @@ "dependencies": { "@balena/dockerignore": "^1.0.2", "@types/dockerode": "^3.3.29", + "@types/retry": "^0.12.5", "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", diff --git a/packages/testcontainers/src/common/file-lock.ts b/packages/testcontainers/src/common/file-lock.ts index 8ee33034b..3e0b206c6 100644 --- a/packages/testcontainers/src/common/file-lock.ts +++ b/packages/testcontainers/src/common/file-lock.ts @@ -2,14 +2,19 @@ import path from "path"; import { writeFile } from "fs/promises"; import lockFile from "proper-lockfile"; import { log } from "./logger"; +import type { WrapOptions } from "retry"; -export async function withFileLock(fileName: string, fn: () => T): Promise { +export async function withFileLock( + fileName: string, + fn: () => T, + options?: { retryOptions: Omit } +): Promise { const file = await createEmptyTmpFile(fileName); let releaseLockFn; try { log.debug(`Acquiring lock file "${file}"...`); - releaseLockFn = await lockFile.lock(file, { retries: { forever: true } }); + releaseLockFn = await lockFile.lock(file, { retries: { ...options?.retryOptions, forever: true } }); log.debug(`Acquired lock file "${file}"`); return await fn(); } finally { diff --git a/packages/testcontainers/src/common/index.ts b/packages/testcontainers/src/common/index.ts index fccab29fb..b851dc63f 100644 --- a/packages/testcontainers/src/common/index.ts +++ b/packages/testcontainers/src/common/index.ts @@ -5,3 +5,4 @@ export { Uuid, RandomUuid } from "./uuid"; export { streamToString } from "./streams"; export { withFileLock } from "./file-lock"; export { Retry, IntervalRetry } from "./retry"; +export { setRetryOptions, getRetryOptions } from "./preferences"; diff --git a/packages/testcontainers/src/common/preferences.ts b/packages/testcontainers/src/common/preferences.ts new file mode 100644 index 000000000..e248f80d0 --- /dev/null +++ b/packages/testcontainers/src/common/preferences.ts @@ -0,0 +1,11 @@ +import type { WrapOptions } from "retry"; + +let retryOptions: WrapOptions = {}; + +export function setRetryOptions(retryOptionsInput: Omit): void { + retryOptions = retryOptionsInput; +} + +export function getRetryOptions(): WrapOptions { + return retryOptions; +} diff --git a/packages/testcontainers/src/reaper/reaper.ts b/packages/testcontainers/src/reaper/reaper.ts index fd9fdf87b..a8b0fec90 100644 --- a/packages/testcontainers/src/reaper/reaper.ts +++ b/packages/testcontainers/src/reaper/reaper.ts @@ -3,7 +3,7 @@ import { GenericContainer } from "../generic-container/generic-container"; import { Wait } from "../wait-strategies/wait"; import { Socket } from "net"; import { ContainerRuntimeClient, ImageName } from "../container-runtime"; -import { IntervalRetry, log, RandomUuid, withFileLock } from "../common"; +import { getRetryOptions, IntervalRetry, log, RandomUuid, withFileLock } from "../common"; import { LABEL_TESTCONTAINERS_SESSION_ID } from "../utils/labels"; export const REAPER_IMAGE = process.env["RYUK_CONTAINER_IMAGE"] @@ -26,18 +26,22 @@ export async function getReaper(client: ContainerRuntimeClient): Promise return reaper; } - reaper = await withFileLock("testcontainers-node.lock", async () => { - const reaperContainer = await findReaperContainer(client); - sessionId = reaperContainer?.Labels["org.testcontainers.session-id"] ?? new RandomUuid().nextUuid(); - - if (process.env.TESTCONTAINERS_RYUK_DISABLED === "true") { - return new DisabledReaper(sessionId); - } else if (reaperContainer) { - return await useExistingReaper(reaperContainer, sessionId, client.info.containerRuntime.host); - } else { - return await createNewReaper(sessionId, client.info.containerRuntime.remoteSocketPath); - } - }); + reaper = await withFileLock( + "testcontainers-node.lock", + async () => { + const reaperContainer = await findReaperContainer(client); + sessionId = reaperContainer?.Labels["org.testcontainers.session-id"] ?? new RandomUuid().nextUuid(); + + if (process.env.TESTCONTAINERS_RYUK_DISABLED === "true") { + return new DisabledReaper(sessionId); + } else if (reaperContainer) { + return await useExistingReaper(reaperContainer, sessionId, client.info.containerRuntime.host); + } else { + return await createNewReaper(sessionId, client.info.containerRuntime.remoteSocketPath); + } + }, + { retryOptions: getRetryOptions() } + ); reaper.addSession(sessionId); return reaper; diff --git a/packages/testcontainers/src/test-containers.ts b/packages/testcontainers/src/test-containers.ts index 9c2718fea..ba271c7ec 100644 --- a/packages/testcontainers/src/test-containers.ts +++ b/packages/testcontainers/src/test-containers.ts @@ -1,6 +1,7 @@ import { PortForwarderInstance } from "./port-forwarder/port-forwarder"; import { getContainerRuntimeClient } from "./container-runtime"; import { log } from "./common"; +import type { WrapOptions } from "retry"; export class TestContainers { public static async exposeHostPorts(...ports: number[]): Promise { @@ -19,6 +20,10 @@ export class TestContainers { ); } + public static setLockFileRetryOptions(retryOptions: Omit): void { + retryOptions; + } + private static async isHostPortExposed(portForwarderContainerId: string, hostPort: number): Promise { const client = await getContainerRuntimeClient(); const container = client.container.getById(portForwarderContainerId);