Skip to content

Commit

Permalink
feat(file-lock): Added configurable retries for file-lock
Browse files Browse the repository at this point in the history
  • Loading branch information
GiDW committed Jan 6, 2025
1 parent ba73759 commit 510969c
Show file tree
Hide file tree
Showing 6 changed files with 42 additions and 15 deletions.
1 change: 1 addition & 0 deletions packages/testcontainers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 7 additions & 2 deletions packages/testcontainers/src/common/file-lock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(fileName: string, fn: () => T): Promise<T> {
export async function withFileLock<T>(
fileName: string,
fn: () => T,
options?: { retryOptions: Omit<WrapOptions, "forever"> }
): Promise<T> {
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 {
Expand Down
1 change: 1 addition & 0 deletions packages/testcontainers/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
11 changes: 11 additions & 0 deletions packages/testcontainers/src/common/preferences.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { WrapOptions } from "retry";

let retryOptions: WrapOptions = {};

export function setRetryOptions(retryOptionsInput: Omit<WrapOptions, "forever">): void {
retryOptions = retryOptionsInput;
}

export function getRetryOptions(): WrapOptions {
return retryOptions;
}
30 changes: 17 additions & 13 deletions packages/testcontainers/src/reaper/reaper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand All @@ -26,18 +26,22 @@ export async function getReaper(client: ContainerRuntimeClient): Promise<Reaper>
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;
Expand Down
5 changes: 5 additions & 0 deletions packages/testcontainers/src/test-containers.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
Expand All @@ -19,6 +20,10 @@ export class TestContainers {
);
}

public static setLockFileRetryOptions(retryOptions: Omit<WrapOptions, "forever">): void {
retryOptions;
}

private static async isHostPortExposed(portForwarderContainerId: string, hostPort: number): Promise<boolean> {
const client = await getContainerRuntimeClient();
const container = client.container.getById(portForwarderContainerId);
Expand Down

0 comments on commit 510969c

Please sign in to comment.