Skip to content

Commit

Permalink
Make HealthCheckWaitStrategy event based (#789)
Browse files Browse the repository at this point in the history
  • Loading branch information
cristianrgreco authored Jul 26, 2024
1 parent 3bafc47 commit 6152416
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 18 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: "3.5"

services:
container:
image: cristianrgreco/testcontainer:1.1.14
ports:
- 8080
healthcheck:
test: "curl -f http://localhost:8081/hello-world || exit 1"
timeout: 3s
interval: 1s
retries: 0
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export interface ContainerClient {
logs(container: Container, opts?: ContainerLogsOptions): Promise<Readable>;
exec(container: Container, command: string[], opts?: Partial<ExecOptions>): Promise<ExecResult>;
restart(container: Container, opts?: { timeout: number }): Promise<void>;
events(container: Container, eventNames: string[]): Promise<Readable>;
remove(container: Container, opts?: { removeVolumes: boolean }): Promise<void>;
connectToNetwork(container: Container, network: Network, networkAliases: string[]): Promise<void>;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { IncomingMessage } from "http";
import { ExecOptions, ExecResult } from "./types";
import byline from "byline";
import { ContainerClient } from "./container-client";
import { log, execLog, streamToString } from "../../../common";
import { execLog, log, streamToString } from "../../../common";

export class DockerContainerClient implements ContainerClient {
constructor(public readonly dockerode: Dockerode) {}
Expand Down Expand Up @@ -248,6 +248,19 @@ export class DockerContainerClient implements ContainerClient {
}
}

async events(container: Container, eventNames: string[]): Promise<Readable> {
log.debug(`Fetching event stream...`, { containerId: container.id });
const stream = (await this.dockerode.getEvents({
filters: {
type: ["container"],
container: [container.id],
event: eventNames,
},
})) as Readable;
log.debug(`Fetched event stream...`, { containerId: container.id });
return stream;
}

protected async demuxStream(containerId: string, stream: Readable): Promise<Readable> {
try {
log.debug(`Demuxing stream...`, { containerId });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,14 @@ describe("DockerComposeEnvironment", () => {
await startedEnvironment.down();
});

it("should support failing health check wait strategy", async () => {
await expect(
new DockerComposeEnvironment(fixtures, "docker-compose-with-healthcheck-unhealthy.yml")
.withWaitStrategy(await composeContainerName("container"), Wait.forHealthCheck())
.up()
).rejects.toThrow(`Health check failed: unhealthy`);
});

if (!process.env.CI_PODMAN) {
it("should stop the container when the health check wait strategy times out", async () => {
await expect(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,47 @@
import Dockerode from "dockerode";
import { AbstractWaitStrategy } from "./wait-strategy";
import { IntervalRetry, log } from "../common";
import { log } from "../common";
import { getContainerRuntimeClient } from "../container-runtime";

export class HealthCheckWaitStrategy extends AbstractWaitStrategy {
public async waitUntilReady(container: Dockerode.Container): Promise<void> {
log.debug(`Waiting for health check...`, { containerId: container.id });

const client = await getContainerRuntimeClient();
const containerEvents = await client.container.events(container, ["health_status"]);

const status = await new IntervalRetry<string | undefined, Error>(100).retryUntil(
async () => (await client.container.inspect(container)).State.Health?.Status,
(healthCheckStatus) => healthCheckStatus === "healthy" || healthCheckStatus === "unhealthy",
() => {
const timeout = this.startupTimeout;
const message = `Health check not healthy after ${timeout}ms`;
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
const message = `Health check not healthy after ${this.startupTimeout}ms`;
log.error(message, { containerId: container.id });
throw new Error(message);
},
this.startupTimeout
);
containerEvents.destroy();
reject(new Error(message));
}, this.startupTimeout);

const onTerminalState = () => {
clearTimeout(timeout);
containerEvents.destroy();
log.debug(`Health check wait strategy complete`, { containerId: container.id });
};

containerEvents.on("data", (data) => {
const parsedData = JSON.parse(data);

if (status !== "healthy") {
const message = `Health check failed: ${status}`;
log.error(message, { containerId: container.id });
throw new Error(message);
}
const status =
parsedData.status.split(":").length === 2
? parsedData.status.split(":")[1].trim() // Docker
: parsedData.HealthStatus; // Podman

log.debug(`Health check wait strategy complete`, { containerId: container.id });
if (status === "healthy") {
resolve();
onTerminalState();
} else if (status === "unhealthy") {
const message = `Health check failed: ${status}`;
log.error(message, { containerId: container.id });
reject(new Error(message));
onTerminalState();
}
});
});
}
}

0 comments on commit 6152416

Please sign in to comment.