Skip to content

Commit 7f0501c

Browse files
authored
Container stop timeouts should be in milliseconds (#962)
1 parent c9ded05 commit 7f0501c

File tree

20 files changed

+109
-83
lines changed

20 files changed

+109
-83
lines changed

docs/features/compose.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,11 @@ const environment = await new DockerComposeEnvironment(composeFilePath, composeF
151151
await environment.down();
152152
```
153153

154-
If you need to wait for the environment to be downed, you can provide a timeout. The unit of timeout here is **second**:
154+
If you need to wait for the environment to be downed, you can provide a timeout:
155155

156156
```javascript
157157
const environment = await new DockerComposeEnvironment(composeFilePath, composeFile).up();
158-
await environment.down({ timeout: 10 }); // timeout after 10 seconds
158+
await environment.down({ timeout: 10_000 }); // 10 seconds
159159
```
160160

161161
Volumes created by the environment are removed when stopped. This is configurable:

docs/features/containers.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -334,11 +334,11 @@ const container = await new GenericContainer("alpine").start();
334334
await container.stop();
335335
```
336336

337-
If you need to wait for the container to be stopped, you can provide a timeout. The unit of timeout option here is **second**:
337+
If you need to wait for the container to be stopped, you can provide a timeout:
338338

339339
```javascript
340340
const container = await new GenericContainer("alpine").start();
341-
await container.stop({ timeout: 10 }); // 10 seconds
341+
await container.stop({ timeout: 10_000 }); // 10 seconds
342342
```
343343

344344
You can disable automatic removal of the container, which is useful for debugging, or if for example you want to copy content from the container once it has stopped:

docs/features/wait-strategies.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Wait Strategies
22

3-
Note that the startup timeout of all wait strategies is configurable. The unit of timeout of wait strategies is **millisecond**:
3+
Note that the startup timeout of all wait strategies is configurable:
44

55
```javascript
66
const { GenericContainer } = require("testcontainers");
77

88
const container = await new GenericContainer("alpine")
9-
.withStartupTimeout(120000) // wait 120 seconds
9+
.withStartupTimeout(120_000) // 120 seconds
1010
.start();
1111
```
1212

@@ -73,18 +73,18 @@ const { GenericContainer, Wait } = require("testcontainers");
7373
const container = await new GenericContainer("alpine").withWaitStrategy(Wait.forHealthCheck()).start();
7474
```
7575

76-
Define your own health check. The unit of timeouts and intervals here is **millisecond**:
76+
Define your own health check:
7777

7878
```javascript
7979
const { GenericContainer, Wait } = require("testcontainers");
8080

8181
const container = await new GenericContainer("alpine")
8282
.withHealthCheck({
8383
test: ["CMD-SHELL", "curl -f http://localhost || exit 1"],
84-
interval: 1000,
85-
timeout: 3000,
84+
interval: 1000, // 1 second
85+
timeout: 3000, // 3 seconds
8686
retries: 5,
87-
startPeriod: 1000,
87+
startPeriod: 1000, // 1 second
8888
})
8989
.withWaitStrategy(Wait.forHealthCheck())
9090
.start();
@@ -148,7 +148,7 @@ const container = await new GenericContainer("redis")
148148
.withMethod("POST")
149149
.withHeaders({ X_CUSTOM_VALUE: "custom" })
150150
.withBasicCredentials("username", "password")
151-
.withReadTimeout(10000)) // timeout after 10 seconds
151+
.withReadTimeout(10_000)) // 10 seconds
152152
```
153153

154154
### Use TLS
@@ -186,7 +186,7 @@ This strategy is intended for use with containers that only run briefly and exit
186186
const { GenericContainer, Wait } = require("testcontainers");
187187

188188
const container = await new GenericContainer("alpine")
189-
.withWaitStrategy(Wait.forOneShotStartup()))
189+
.withWaitStrategy(Wait.forOneShotStartup())
190190
.start();
191191
```
192192

@@ -202,11 +202,11 @@ const container = await new GenericContainer("alpine")
202202
.start();
203203
```
204204

205-
The composite wait strategy by default will respect each individual wait strategy's startup timeout. The unit of timeouts here is **millisecond**. For example:
205+
The composite wait strategy by default will respect each individual wait strategy's startup timeout. For example:
206206

207207
```javascript
208-
const w1 = Wait.forListeningPorts().withStartupTimeout(1000); // wait 1 second
209-
const w2 = Wait.forLogMessage("READY").withStartupTimeout(2000); // wait 2 seconds
208+
const w1 = Wait.forListeningPorts().withStartupTimeout(1000); // 1 second
209+
const w2 = Wait.forLogMessage("READY").withStartupTimeout(2000); // 2 seconds
210210

211211
const composite = Wait.forAll([w1, w2]);
212212

@@ -217,21 +217,21 @@ expect(w2.getStartupTimeout()).toBe(2000);
217217
The startup timeout of inner wait strategies that have not defined their own startup timeout can be set by setting the startup timeout on the composite:
218218

219219
```javascript
220-
const w1 = Wait.forListeningPorts().withStartupTimeout(1000); // wait 1 second
220+
const w1 = Wait.forListeningPorts().withStartupTimeout(1000); // 1 second
221221
const w2 = Wait.forLogMessage("READY");
222222

223-
const composite = Wait.forAll([w1, w2]).withStartupTimeout(2000); // wait 2 seconds
223+
const composite = Wait.forAll([w1, w2]).withStartupTimeout(2000); // 2 seconds
224224

225225
expect(w1.getStartupTimeout()).toBe(1000);
226226
expect(w2.getStartupTimeout()).toBe(2000);
227227
```
228228

229-
The startup timeout of all wait strategies can be controlled by setting a deadline on the composite. In this case, the composite will throw unless all inner wait strategies have resolved before the deadline. The unit of deadline timeout is **millisecond**.
229+
The startup timeout of all wait strategies can be controlled by setting a deadline on the composite. In this case, the composite will throw unless all inner wait strategies have resolved before the deadline.
230230

231231
```javascript
232232
const w1 = Wait.forListeningPorts();
233233
const w2 = Wait.forLogMessage("READY");
234-
const composite = Wait.forAll([w1, w2]).withDeadline(2000); // wait 2 seconds
234+
const composite = Wait.forAll([w1, w2]).withDeadline(2000); // 2 seconds
235235
```
236236

237237
## Other startup strategies

packages/modules/couchbase/src/couchbase-container.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -448,12 +448,12 @@ export class CouchbaseContainer extends GenericContainer {
448448
return jsonResponse.results[0].present;
449449
},
450450
() => {
451-
const message = `URL /query/service not accessible after ${this.startupTimeout || 60_000}ms`;
451+
const message = `URL /query/service not accessible after ${this.startupTimeoutMs || 60_000}ms`;
452452
log.error(message, { containerId: client.container.getById(startedTestContainer.getId()).id });
453453

454454
throw new Error(message);
455455
},
456-
this.startupTimeout || 60_000
456+
this.startupTimeoutMs || 60_000
457457
);
458458
}
459459

@@ -509,12 +509,12 @@ export class CouchbaseContainer extends GenericContainer {
509509
return jsonResponse.results[0].online;
510510
},
511511
() => {
512-
const message = `URL /query/service not accessible after ${this.startupTimeout || 60_000}ms`;
512+
const message = `URL /query/service not accessible after ${this.startupTimeoutMs || 60_000}ms`;
513513
log.error(message, { containerId: client.container.getById(startedTestContainer.getId()).id });
514514

515515
throw new Error(message);
516516
},
517-
this.startupTimeout || 60_000
517+
this.startupTimeoutMs || 60_000
518518
);
519519
} else {
520520
log.info(

packages/testcontainers/src/common/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ export { hash } from "./hash";
33
export { Logger, buildLog, composeLog, containerLog, execLog, log, pullLog } from "./logger";
44
export { IntervalRetry, Retry } from "./retry";
55
export { streamToString } from "./streams";
6+
export * from "./time";
67
export * from "./type-guards";
78
export { RandomUuid, Uuid } from "./uuid";

packages/testcontainers/src/common/retry.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ export interface Retry<T, U> {
55
fn: () => Promise<T>,
66
predicate: (result: T) => boolean | Promise<boolean>,
77
onTimeout: () => U,
8-
timeout: number
8+
timeoutMs: number
99
): Promise<T | U>;
1010
}
1111

@@ -16,11 +16,11 @@ abstract class AbstractRetry<T, U> implements Retry<T, U> {
1616
fn: () => Promise<T>,
1717
predicate: (result: T) => boolean | Promise<boolean>,
1818
onTimeout: () => U,
19-
timeout: number
19+
timeoutMs: number
2020
): Promise<T | U>;
2121

22-
protected hasTimedOut(timeout: number, startTime: Time): boolean {
23-
return this.clock.getTime() - startTime > timeout;
22+
protected hasTimedOut(timeoutMs: number, startTime: Time): boolean {
23+
return this.clock.getTime() - startTime > timeoutMs;
2424
}
2525

2626
protected wait(duration: number): Promise<void> {
@@ -37,15 +37,15 @@ export class IntervalRetry<T, U> extends AbstractRetry<T, U> {
3737
fn: (attempt: number) => Promise<T>,
3838
predicate: (result: T) => boolean | Promise<boolean>,
3939
onTimeout: () => U,
40-
timeout: number
40+
timeoutMs: number
4141
): Promise<T | U> {
4242
const startTime = this.clock.getTime();
4343

4444
let attemptNumber = 0;
4545
let result = await fn(attemptNumber++);
4646

4747
while (!(await predicate(result))) {
48-
if (this.hasTimedOut(timeout, startTime)) {
48+
if (this.hasTimedOut(timeoutMs, startTime)) {
4949
return onTimeout();
5050
}
5151
await this.wait(this.interval);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { toNanos, toSeconds } from "./time";
2+
3+
test.for([
4+
[0, 0],
5+
[10, 0],
6+
[999, 0],
7+
[1010, 1],
8+
[1999, 1],
9+
[10_000, 10],
10+
[-10, -0],
11+
[-999, -0],
12+
[-1010, -1],
13+
[-1999, -1],
14+
[-10_000, -10],
15+
])("should convert %i ms to %i seconds", ([ms, s]) => {
16+
expect(toSeconds(ms)).toEqual(s);
17+
});
18+
19+
test.for([
20+
[0, 0],
21+
[1, 1_000_000],
22+
[-1, -1_000_000],
23+
])("should convert %i ms to %i ns", ([ms, ns]) => {
24+
expect(toNanos(ms)).toEqual(ns);
25+
});
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export const toSeconds = (ms: number) => Math.trunc(ms * 1e-3);
2+
3+
export const toNanos = (ms: number) => ms * 1e6;

packages/testcontainers/src/container-runtime/clients/compose/compose-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import compose from "docker-compose";
2-
import { log, pullLog } from "../../../common";
2+
import { log, pullLog, toSeconds } from "../../../common";
33
import { defaultComposeOptions } from "./default-compose-options";
44
import { ComposeDownOptions, ComposeOptions } from "./types";
55

@@ -126,7 +126,7 @@ function composeDownCommandOptions(options: ComposeDownOptions): string[] {
126126
result.push("-v");
127127
}
128128
if (options.timeout) {
129-
result.push("-t", `${options.timeout / 1000}`);
129+
result.push("-t", `${toSeconds(options.timeout)}`);
130130
}
131131
return result;
132132
}

packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import Dockerode, {
99
} from "dockerode";
1010
import { IncomingMessage } from "http";
1111
import { PassThrough, Readable } from "stream";
12-
import { execLog, log, streamToString } from "../../../common";
12+
import { execLog, log, streamToString, toSeconds } from "../../../common";
1313
import { ContainerClient } from "./container-client";
1414
import { ContainerCommitOptions, ContainerStatus, ExecOptions, ExecResult } from "./types";
1515

@@ -120,8 +120,7 @@ export class DockerContainerClient implements ContainerClient {
120120

121121
async inspect(container: Dockerode.Container): Promise<ContainerInspectInfo> {
122122
try {
123-
const inspectInfo = await container.inspect();
124-
return inspectInfo;
123+
return await container.inspect();
125124
} catch (err) {
126125
log.error(`Failed to inspect container: ${err}`, { containerId: container.id });
127126
throw err;
@@ -131,7 +130,7 @@ export class DockerContainerClient implements ContainerClient {
131130
async stop(container: Container, opts?: { timeout: number }): Promise<void> {
132131
try {
133132
log.debug(`Stopping container...`, { containerId: container.id });
134-
await container.stop({ t: opts?.timeout });
133+
await container.stop({ t: toSeconds(opts?.timeout ?? 0) });
135134
log.debug(`Stopped container`, { containerId: container.id });
136135
// eslint-disable-next-line @typescript-eslint/no-explicit-any
137136
} catch (err: any) {
@@ -267,7 +266,7 @@ export class DockerContainerClient implements ContainerClient {
267266
async restart(container: Container, opts?: { timeout: number }): Promise<void> {
268267
try {
269268
log.debug(`Restarting container...`, { containerId: container.id });
270-
await container.restart({ t: opts?.timeout });
269+
await container.restart({ t: toSeconds(opts?.timeout ?? 0) });
271270
log.debug(`Restarted container`, { containerId: container.id });
272271
} catch (err) {
273272
log.error(`Failed to restart container: ${err}`, { containerId: container.id });

0 commit comments

Comments
 (0)