Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(file-lock): Added configurable retries for file-lock #886

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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