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

Solves: 5359 by using a more flexible wait strategy #5501

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package org.testcontainers.containers.wait.experimental;

import org.rnorth.ducttape.unreliables.Unreliables;
import org.testcontainers.containers.output.WaitingConsumer;
import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;
import org.testcontainers.containers.wait.strategy.WaitStrategy;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;


public class ComplexWaitStrategy extends AbstractWaitStrategy {

private final List<WaitStrategy> waitStrategyList = new ArrayList<>();

@Override
protected void waitUntilReady() {
ExecutorService service = Executors.newFixedThreadPool(waitStrategyList.size());
List<? extends Future<?>> futures = waitStrategyList.stream()
.map(waitStrategy -> service.submit(() -> waitStrategy.waitUntilReady(waitStrategyTarget)))
.collect(Collectors.toList());

Unreliables.retryUntilTrue(
(int) startupTimeout.getSeconds(),
TimeUnit.SECONDS,
() -> futures.stream().anyMatch(Future::isDone)
);
}

public ComplexWaitStrategy with(WaitStrategy waitStrategy) {
this.waitStrategyList.add(waitStrategy);
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package org.testcontainers.containers.wait.experimental;

import com.github.dockerjava.api.command.LogContainerCmd;
import lombok.SneakyThrows;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.output.FrameConsumerResultCallback;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.WaitingConsumer;
import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;

import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;

/*
This wait strategy has following characteristics
- Validates the regex which has been configured with the entire log history. It does this by using a StringBuffer (thread safety) to concat everything together.
- Logic for "x amount of times" or complex setups such as "(a OR b) AND c" is fully to be implemented by regex.
- "times" is removed from the class interface.

Risks:
- Slower for extremely long logs lines.
*/

public class HoggingLogMessageWaitStrategy extends AbstractWaitStrategy {

// Hardcoded, as we assume the regEx will check the amount of times for this implementation.
private static final int CHECK_TIMES = 1;

private String regEx;

@Override
@SneakyThrows(IOException.class)
protected void waitUntilReady() {
WaitingConsumer waitingConsumer = new WaitingConsumer();

LogContainerCmd cmd = waitStrategyTarget
.getDockerClient()
.logContainerCmd(waitStrategyTarget.getContainerId())
.withFollowStream(true)
.withSince(0)
.withStdOut(true)
.withStdErr(true);

try (FrameConsumerResultCallback callback = new FrameConsumerResultCallback()) {
callback.addConsumer(OutputFrame.OutputType.STDOUT, waitingConsumer);
callback.addConsumer(OutputFrame.OutputType.STDERR, waitingConsumer);

cmd.exec(callback);

//region Specific Implementation

final StringBuffer builder = new StringBuffer();

Predicate<OutputFrame> waitPredicate = outputFrame -> {
builder.append(outputFrame.getUtf8String());
// (?s) enables line terminator matching (equivalent to Pattern.DOTALL)
return builder.toString().matches("(?s)" + regEx);
};

//endregion

try {
waitingConsumer.waitUntil(waitPredicate, startupTimeout.getSeconds(), TimeUnit.SECONDS, CHECK_TIMES);
} catch (TimeoutException e) {
throw new ContainerLaunchException("Timed out waiting for log output matching '" + regEx + "'");
}
}
}

public HoggingLogMessageWaitStrategy withRegEx(String regEx) {
this.regEx = regEx;
return this;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package org.testcontainers.containers.wait.experimental;

import com.github.dockerjava.api.command.LogContainerCmd;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.output.FrameConsumerResultCallback;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.WaitingConsumer;
import org.testcontainers.containers.wait.strategy.AbstractLogMessageWaitStrategy;
import org.testcontainers.containers.wait.strategy.AbstractWaitStrategy;

import java.io.IOException;
import java.util.Arrays;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/*
This wait strategy has following characteristics
- Allows multiple regex to be added, which will be validated in order.
- Logic for "x amount of times" or complex setups such as "(a OR b) AND c" is fully to be implemented by regex (multiple if needed).
- "times" is removed from the class interface.
*/

@Slf4j
public class MultiLogMessageWaitStrategy extends AbstractLogMessageWaitStrategy {

private LinkedBlockingDeque<String> regEx;

@Override
protected Predicate<OutputFrame> waitPredicate() {
return outputFrame -> {
String nextExpectedRegex = regEx.peek();
// (?s) enables line terminator matching (equivalent to Pattern.DOTALL)
if(outputFrame.getUtf8String().matches("(?s)" + nextExpectedRegex)) {
// remove the matched item from the collection.
regEx.pop();
log.info("Regex {} encountered. Waiting for {} more regex statements.", nextExpectedRegex, regEx.size());
}
// If collection is now empty, we are finished.
return regEx.isEmpty();
};
}

@Override
protected String timeoutErrorMessage() {
return "Timed out waiting for log output. Still expecting following log lines: '" + String.join(",", regEx) + "'";
}

public MultiLogMessageWaitStrategy withRegex(String... regEx) {
this.regEx = new LinkedBlockingDeque<>(Arrays.asList(regEx));
return this;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.testcontainers.containers.wait.strategy;

import com.github.dockerjava.api.command.LogContainerCmd;
import lombok.SneakyThrows;
import org.testcontainers.containers.ContainerLaunchException;
import org.testcontainers.containers.output.FrameConsumerResultCallback;
import org.testcontainers.containers.output.OutputFrame;
import org.testcontainers.containers.output.WaitingConsumer;

import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;

public abstract class AbstractLogMessageWaitStrategy extends AbstractWaitStrategy {

protected int times = 1;

@Override
@SneakyThrows(IOException.class)
protected void waitUntilReady() {
WaitingConsumer waitingConsumer = new WaitingConsumer();

LogContainerCmd cmd = waitStrategyTarget
.getDockerClient()
.logContainerCmd(waitStrategyTarget.getContainerId())
.withFollowStream(true)
.withSince(0)
.withStdOut(true)
.withStdErr(true);

try (FrameConsumerResultCallback callback = new FrameConsumerResultCallback()) {
callback.addConsumer(OutputFrame.OutputType.STDOUT, waitingConsumer);
callback.addConsumer(OutputFrame.OutputType.STDERR, waitingConsumer);

cmd.exec(callback);

try {
waitingConsumer.waitUntil(waitPredicate(), startupTimeout.getSeconds(), TimeUnit.SECONDS, times);
} catch (TimeoutException e) {
throw new ContainerLaunchException(timeoutErrorMessage());
}
}
}

protected abstract Predicate<OutputFrame> waitPredicate();
protected abstract String timeoutErrorMessage();

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,41 +12,21 @@
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;

public class LogMessageWaitStrategy extends AbstractWaitStrategy {
public class LogMessageWaitStrategy extends AbstractLogMessageWaitStrategy {

private String regEx;

private int times = 1;
@Override
protected Predicate<OutputFrame> waitPredicate() {
return outputFrame -> {
// (?s) enables line terminator matching (equivalent to Pattern.DOTALL)
return outputFrame.getUtf8String().matches("(?s)" + regEx);
};
}

@Override
@SneakyThrows(IOException.class)
protected void waitUntilReady() {
WaitingConsumer waitingConsumer = new WaitingConsumer();

LogContainerCmd cmd = waitStrategyTarget
.getDockerClient()
.logContainerCmd(waitStrategyTarget.getContainerId())
.withFollowStream(true)
.withSince(0)
.withStdOut(true)
.withStdErr(true);

try (FrameConsumerResultCallback callback = new FrameConsumerResultCallback()) {
callback.addConsumer(OutputFrame.OutputType.STDOUT, waitingConsumer);
callback.addConsumer(OutputFrame.OutputType.STDERR, waitingConsumer);

cmd.exec(callback);

Predicate<OutputFrame> waitPredicate = outputFrame -> {
// (?s) enables line terminator matching (equivalent to Pattern.DOTALL)
return outputFrame.getUtf8String().matches("(?s)" + regEx);
};
try {
waitingConsumer.waitUntil(waitPredicate, startupTimeout.getSeconds(), TimeUnit.SECONDS, times);
} catch (TimeoutException e) {
throw new ContainerLaunchException("Timed out waiting for log output matching '" + regEx + "'");
}
}
protected String timeoutErrorMessage() {
return "Timed out waiting for log output matching '" + regEx + "'";
}

public LogMessageWaitStrategy withRegEx(String regEx) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.testcontainers.containers;

import org.jetbrains.annotations.NotNull;
import org.testcontainers.containers.wait.experimental.ComplexWaitStrategy;
import org.testcontainers.containers.wait.experimental.MultiLogMessageWaitStrategy;
import org.testcontainers.containers.wait.strategy.LogMessageWaitStrategy;
import org.testcontainers.utility.DockerImageName;

Expand Down Expand Up @@ -49,10 +51,16 @@ public PostgreSQLContainer(final DockerImageName dockerImageName) {
dockerImageName.assertCompatibleWith(DEFAULT_IMAGE_NAME);

this.waitStrategy =
new LogMessageWaitStrategy()
.withRegEx(".*database system is ready to accept connections.*\\s")
.withTimes(2)
.withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS));
new ComplexWaitStrategy()
.with(new LogMessageWaitStrategy()
.withRegEx(".*database system is ready to accept connections.*\\s")
.withTimes(2)
.withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)))
.with(new MultiLogMessageWaitStrategy()
.withRegex(".*PostgreSQL Database directory appears to contain a database; Skipping initialization.*",
".*database system is ready to accept connections.*\\s")
.withStartupTimeout(Duration.of(60, ChronoUnit.SECONDS)))
;
this.setCommand("postgres", "-c", FSYNC_OFF_OPTION);

addExposedPort(POSTGRESQL_PORT);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@
import org.testcontainers.PostgreSQLTestImages;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.db.AbstractContainerDatabaseTest;
import org.testcontainers.utility.DockerImageName;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.logging.Level;
import java.util.logging.LogManager;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;

public class SimplePostgreSQLTest extends AbstractContainerDatabaseTest {
static {
Expand All @@ -20,7 +22,23 @@ public class SimplePostgreSQLTest extends AbstractContainerDatabaseTest {

@Test
public void testSimple() throws SQLException {
try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(PostgreSQLTestImages.POSTGRES_TEST_IMAGE)) {
try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(DockerImageName.parse("postgres:14"))) {
postgres.start();

ResultSet resultSet = performQuery(postgres, "SELECT 1");
int resultSetInt = resultSet.getInt(1);
assertEquals("A basic SELECT query succeeds", 1, resultSetInt);
}
}

@Test
public void testSimpleWithData() throws SQLException {
DockerImageName IMAGE = DockerImageName.parse("tomcools/postgres:main")
.asCompatibleSubstituteFor("postgres");
try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>(IMAGE)
.withDatabaseName("testcontainer")
.withUsername("sa")
.withPassword("sa")) {
postgres.start();

ResultSet resultSet = performQuery(postgres, "SELECT 1");
Expand Down