Skip to content
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,8 @@ To check if the test container is running, call the `isRunning` method:
var isRunning = container.isRunning();
```

`start` waits until the database answers a ping, for up to 30 seconds from creating the container. Pulling the image does not count. If the container runs, but the database does not become reachable in time, `start` replaces the container with a new one, up to 3 containers in total. If Docker fails, or if the container exits, `start` fails at once, and in the latter case names the last lines of the container's log. Whenever a container fails to start, `start` removes it.

To get a client with options of your own, e.g. the data mapper of your application, hand them to `getClient`:

```java
Expand Down Expand Up @@ -834,6 +836,13 @@ var container = new Container()
.withApiToken("secret");
```

To change how long `start` waits for the database in each container, call the `withStartupTimeout` method. The default is 30 seconds:

```java
var container = new Container()
.withStartupTimeout(Duration.ofSeconds(60));
```

If you want to sign events, call the `withSigningKey` method. This generates a new signing and verification key pair, and hands the signing key to the container:

```java
Expand Down
6 changes: 6 additions & 0 deletions eventsourcingdb-testcontainers/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ dependencies {
api(project(":eventsourcingdb"))
api(libs.testcontainers)
}

// Testcontainers reuses containers only if the environment allows it, which
// the tests of reuse need. It affects only containers that ask for reuse.
tasks.test {
environment("TESTCONTAINERS_REUSE_ENABLE", "true")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.thenativeweb.eventsourcingdb.testcontainers;

import java.time.Duration;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
import java.util.function.IntPredicate;
import java.util.function.Supplier;
import org.jspecify.annotations.Nullable;
import org.testcontainers.containers.ContainerLaunchException;

// Attempt is one attempt of Container.start() to start a container. Its clock
// for the startup timeout starts once it is created, which is right before
// Testcontainers asks Docker to create the container, so that pulling the
// image does not count. While Testcontainers starts the container, Container
// notes here what it learns about the container, so that an attempt that
// failed can tell whether to replace the container.
final class Attempt {
// How long the wait for the database pauses between two pings.
private static final Duration POLL_INTERVAL = Duration.ofMillis(100);

// The id of the attempt, which the label of its container carries.
final String id = UUID.randomUUID().toString();

private final long deadline;

// Whether the wait for the database began. Testcontainers begins it only
// once Docker published the port of the container.
boolean hasWaitBegun;

// Whether the wait for the database ran out of time.
boolean hasRunOutOfTime;

// Whether the container was running when Testcontainers gave up on it.
boolean wasRunning;

// The last lines of the log of the container, if it exited.
@Nullable
String exitLog;

Attempt(Duration startupTimeout) {
// Converting saturates, so that a startup timeout too long to count in
// nanoseconds does not fail.
deadline = System.nanoTime() + TimeUnit.NANOSECONDS.convert(startupTimeout);
}

// Waits until the database answers a ping, until the container exits, or
// until the startup timeout runs out. It observes the container anew for
// every ping, including the port that Docker published for the database,
// as Docker sometimes publishes it late. A failure to observe the
// container is one of Docker, which ends the wait at once, and does not
// count as running out of time. That includes an interrupt, which cuts
// the call to Docker short.
void waitUntilReachable(Supplier<Observation> observe, IntPredicate answersPing) {
hasWaitBegun = true;

while (true) {
var observation = observe.get();
if (observation.hasExited()) {
throw new ContainerLaunchException("the container exited");
}
if (System.nanoTime() - deadline >= 0) {
hasRunOutOfTime = true;
throw new ContainerLaunchException("the database did not become reachable in time");
}

var mappedPort = observation.mappedPort();
if (mappedPort != null && answersPing.test(mappedPort)) {
return;
}

LockSupport.parkNanos(POLL_INTERVAL.toNanos());
}
}

// Tells whether the container was running, but the database in it did
// not become reachable in time. That is the case if the wait for the
// database ran out of time, and also if the container was running when
// Testcontainers gave up waiting for Docker to publish its port, which it
// does after a fixed time of its own.
boolean isUnreachable() {
return hasRunOutOfTime || (wasRunning && !hasWaitBegun);
}

// Observation is what a look at the container tells: whether it exited,
// and the port on the host that Docker published for the database, if it
// did yet.
record Observation(boolean hasExited, @Nullable Integer mappedPort) {}
}
Loading
Loading