From 2c1f4b6a526ef445046be420adff7713201013c9 Mon Sep 17 00:00:00 2001 From: Semyon Levin Date: Mon, 13 Apr 2026 13:45:40 +0400 Subject: [PATCH 1/2] Make GenericContainer.start() and stop() thread-safe --- .../containers/GenericContainer.java | 5 +- .../ClickHouseR2DBCDatabaseContainer.java | 3 + .../junit/jupiter/ParallelDependsOnTest.java | 66 +++++++++++++++++++ .../MariaDBR2DBCDatabaseContainer.java | 3 + .../MSSQLR2DBCDatabaseContainer.java | 3 + .../mysql/MySQLR2DBCDatabaseContainer.java | 3 + .../oracle/OracleR2DBCDatabaseContainer.java | 3 + .../PostgreSQLR2DBCDatabaseContainer.java | 3 + .../containers/BrowserWebDriverContainer.java | 2 + .../selenium/BrowserWebDriverContainer.java | 2 + 10 files changed, 92 insertions(+), 1 deletion(-) create mode 100644 modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ParallelDependsOnTest.java diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 0fe944433ae..225b63c0545 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -28,6 +28,7 @@ import lombok.NonNull; import lombok.Setter; import lombok.SneakyThrows; +import lombok.Synchronized; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; @@ -169,7 +170,7 @@ public class GenericContainer> */ @Setter(AccessLevel.NONE) @VisibleForTesting - String containerId; + volatile String containerId; @Setter(AccessLevel.NONE) private InspectContainerResponse containerInfo; @@ -306,6 +307,7 @@ public String getContainerId() { * Starts the container using docker, pulling an image if necessary. */ @Override + @Synchronized @SneakyThrows({ InterruptedException.class, ExecutionException.class }) public void start() { if (containerId != null) { @@ -634,6 +636,7 @@ private void connectToPortForwardingNetwork(String networkMode) { * Kill and remove the container. */ @Override + @Synchronized public void stop() { if (containerId == null) { return; diff --git a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java index be6d8af67c2..21e3d58cb33 100644 --- a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java +++ b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java @@ -1,6 +1,7 @@ package org.testcontainers.clickhouse; import io.r2dbc.spi.ConnectionFactoryOptions; +import lombok.Synchronized; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; /** @@ -24,11 +25,13 @@ public static ConnectionFactoryOptions getOptions(ClickHouseContainer container) } @Override + @Synchronized public void start() { this.container.start(); } @Override + @Synchronized public void stop() { this.container.stop(); } diff --git a/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ParallelDependsOnTest.java b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ParallelDependsOnTest.java new file mode 100644 index 00000000000..5c1d7f0b778 --- /dev/null +++ b/modules/junit-jupiter/src/test/java/org/testcontainers/junit/jupiter/ParallelDependsOnTest.java @@ -0,0 +1,66 @@ +package org.testcontainers.junit.jupiter; + +import org.junit.jupiter.api.Test; +import org.testcontainers.DockerClientFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.lifecycle.Startables; +import org.testcontainers.utility.DockerImageName; + +import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Verifies that concurrent {@link GenericContainer#start()} calls on the same + * container do not result in {@link GenericContainer#doStart()} being + * called more than once. + * + *

A background thread calls {@code container.start()} during class initialization, + * racing with the extension's {@link Startables#deepStart(Stream)} which also starts + * the container as part of the {@code @Container} lifecycle.

+ */ +@Testcontainers(parallel = true) +class ParallelDependsOnTest { + static { + // Pre-initialize the Docker client to increase the chance of a race + // and to emulate a test suite where an earlier test class already + // triggered the initialization. + DockerClientFactory.instance().client(); + } + + private static final AtomicInteger doStartCount = new AtomicInteger(); + + @Container + private static final StartCountingContainer container = new StartCountingContainer( + JUnitJupiterTestImages.HTTPD_IMAGE, + doStartCount + ); + + static { + // Race with the extension's Startables.deepStart. + new Thread(() -> container.start()).start(); + } + + @Test + void containerShouldBeStartedOnlyOnce() { + assertThat(container.isRunning()).isTrue(); + assertThat(doStartCount).as("doStart() invocations").hasValue(1); + } + + private static class StartCountingContainer extends GenericContainer { + + private final AtomicInteger doStartCount; + + StartCountingContainer(DockerImageName image, AtomicInteger doStartCount) { + super(image); + this.doStartCount = doStartCount; + } + + @Override + protected void doStart() { + doStartCount.incrementAndGet(); + super.doStart(); + } + } +} diff --git a/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java b/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java index 4c9e6350d23..5a0977c21a5 100644 --- a/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java +++ b/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java @@ -1,6 +1,7 @@ package org.testcontainers.mariadb; import io.r2dbc.spi.ConnectionFactoryOptions; +import lombok.Synchronized; import org.mariadb.r2dbc.MariadbConnectionFactoryProvider; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @@ -42,11 +43,13 @@ public Set getDependencies() { } @Override + @Synchronized public void start() { this.container.start(); } @Override + @Synchronized public void stop() { this.container.stop(); } diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java b/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java index d8d7740e01d..3324cdf38c0 100644 --- a/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java +++ b/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java @@ -2,6 +2,7 @@ import io.r2dbc.mssql.MssqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryOptions; +import lombok.Synchronized; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @@ -43,11 +44,13 @@ public Set getDependencies() { } @Override + @Synchronized public void start() { this.container.start(); } @Override + @Synchronized public void stop() { this.container.stop(); } diff --git a/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java b/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java index d2a272559d9..95e26c3803d 100644 --- a/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java +++ b/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java @@ -2,6 +2,7 @@ import io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryOptions; +import lombok.Synchronized; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @@ -42,11 +43,13 @@ public Set getDependencies() { } @Override + @Synchronized public void start() { this.container.start(); } @Override + @Synchronized public void stop() { this.container.stop(); } diff --git a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java index ae480027f25..0d7e6e58572 100644 --- a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java +++ b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java @@ -1,6 +1,7 @@ package org.testcontainers.oracle; import io.r2dbc.spi.ConnectionFactoryOptions; +import lombok.Synchronized; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @@ -41,11 +42,13 @@ public Set getDependencies() { } @Override + @Synchronized public void start() { this.container.start(); } @Override + @Synchronized public void stop() { this.container.stop(); } diff --git a/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java b/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java index d99d638b100..4b20c91200d 100644 --- a/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java +++ b/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java @@ -2,6 +2,7 @@ import io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryOptions; +import lombok.Synchronized; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @@ -42,11 +43,13 @@ public Set getDependencies() { } @Override + @Synchronized public void start() { this.container.start(); } @Override + @Synchronized public void stop() { this.container.stop(); } diff --git a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java index 53ee8ba5577..90b1f472997 100644 --- a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java +++ b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java @@ -5,6 +5,7 @@ import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; import com.google.common.collect.ImmutableSet; +import lombok.Synchronized; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; @@ -351,6 +352,7 @@ public void afterTest(TestDescription description, Optional throwable } @Override + @Synchronized public void stop() { if (driver != null) { try { diff --git a/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java b/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java index 97ac23f5d55..1de43ef783b 100644 --- a/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java +++ b/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java @@ -5,6 +5,7 @@ import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; import com.google.common.collect.ImmutableSet; +import lombok.Synchronized; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; @@ -202,6 +203,7 @@ public void afterTest(TestDescription description, Optional throwable } @Override + @Synchronized public void stop() { if (vncRecordingContainer != null) { try { From 9d60e87872f945c3ba9cd00fd7d6be2c17d86497 Mon Sep 17 00:00:00 2001 From: Semyon Levin Date: Thu, 14 May 2026 13:06:48 +0400 Subject: [PATCH 2/2] Replace @Synchronized with synchronized keyword --- .../org/testcontainers/containers/GenericContainer.java | 7 ++----- .../clickhouse/ClickHouseR2DBCDatabaseContainer.java | 7 ++----- .../mariadb/MariaDBR2DBCDatabaseContainer.java | 7 ++----- .../mssqlserver/MSSQLR2DBCDatabaseContainer.java | 7 ++----- .../testcontainers/mysql/MySQLR2DBCDatabaseContainer.java | 7 ++----- .../oracle/OracleR2DBCDatabaseContainer.java | 7 ++----- .../postgresql/PostgreSQLR2DBCDatabaseContainer.java | 7 ++----- .../containers/BrowserWebDriverContainer.java | 4 +--- .../testcontainers/selenium/BrowserWebDriverContainer.java | 4 +--- 9 files changed, 16 insertions(+), 41 deletions(-) diff --git a/core/src/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 225b63c0545..f7954db6369 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -28,7 +28,6 @@ import lombok.NonNull; import lombok.Setter; import lombok.SneakyThrows; -import lombok.Synchronized; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; @@ -307,9 +306,8 @@ public String getContainerId() { * Starts the container using docker, pulling an image if necessary. */ @Override - @Synchronized @SneakyThrows({ InterruptedException.class, ExecutionException.class }) - public void start() { + public synchronized void start() { if (containerId != null) { return; } @@ -636,8 +634,7 @@ private void connectToPortForwardingNetwork(String networkMode) { * Kill and remove the container. */ @Override - @Synchronized - public void stop() { + public synchronized void stop() { if (containerId == null) { return; } diff --git a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java index 21e3d58cb33..9b703fbb9cb 100644 --- a/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java +++ b/modules/clickhouse/src/main/java/org/testcontainers/clickhouse/ClickHouseR2DBCDatabaseContainer.java @@ -1,7 +1,6 @@ package org.testcontainers.clickhouse; import io.r2dbc.spi.ConnectionFactoryOptions; -import lombok.Synchronized; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; /** @@ -25,14 +24,12 @@ public static ConnectionFactoryOptions getOptions(ClickHouseContainer container) } @Override - @Synchronized - public void start() { + public synchronized void start() { this.container.start(); } @Override - @Synchronized - public void stop() { + public synchronized void stop() { this.container.stop(); } diff --git a/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java b/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java index 5a0977c21a5..e840d659799 100644 --- a/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java +++ b/modules/mariadb/src/main/java/org/testcontainers/mariadb/MariaDBR2DBCDatabaseContainer.java @@ -1,7 +1,6 @@ package org.testcontainers.mariadb; import io.r2dbc.spi.ConnectionFactoryOptions; -import lombok.Synchronized; import org.mariadb.r2dbc.MariadbConnectionFactoryProvider; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @@ -43,14 +42,12 @@ public Set getDependencies() { } @Override - @Synchronized - public void start() { + public synchronized void start() { this.container.start(); } @Override - @Synchronized - public void stop() { + public synchronized void stop() { this.container.stop(); } diff --git a/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java b/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java index 3324cdf38c0..7543a2863de 100644 --- a/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java +++ b/modules/mssqlserver/src/main/java/org/testcontainers/mssqlserver/MSSQLR2DBCDatabaseContainer.java @@ -2,7 +2,6 @@ import io.r2dbc.mssql.MssqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryOptions; -import lombok.Synchronized; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @@ -44,14 +43,12 @@ public Set getDependencies() { } @Override - @Synchronized - public void start() { + public synchronized void start() { this.container.start(); } @Override - @Synchronized - public void stop() { + public synchronized void stop() { this.container.stop(); } diff --git a/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java b/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java index 95e26c3803d..b5a08ff6708 100644 --- a/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java +++ b/modules/mysql/src/main/java/org/testcontainers/mysql/MySQLR2DBCDatabaseContainer.java @@ -2,7 +2,6 @@ import io.asyncer.r2dbc.mysql.MySqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryOptions; -import lombok.Synchronized; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @@ -43,14 +42,12 @@ public Set getDependencies() { } @Override - @Synchronized - public void start() { + public synchronized void start() { this.container.start(); } @Override - @Synchronized - public void stop() { + public synchronized void stop() { this.container.stop(); } diff --git a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java index 0d7e6e58572..a178be27539 100644 --- a/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java +++ b/modules/oracle-free/src/main/java/org/testcontainers/oracle/OracleR2DBCDatabaseContainer.java @@ -1,7 +1,6 @@ package org.testcontainers.oracle; import io.r2dbc.spi.ConnectionFactoryOptions; -import lombok.Synchronized; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @@ -42,14 +41,12 @@ public Set getDependencies() { } @Override - @Synchronized - public void start() { + public synchronized void start() { this.container.start(); } @Override - @Synchronized - public void stop() { + public synchronized void stop() { this.container.stop(); } diff --git a/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java b/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java index 4b20c91200d..cb815c2d23f 100644 --- a/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java +++ b/modules/postgresql/src/main/java/org/testcontainers/postgresql/PostgreSQLR2DBCDatabaseContainer.java @@ -2,7 +2,6 @@ import io.r2dbc.postgresql.PostgresqlConnectionFactoryProvider; import io.r2dbc.spi.ConnectionFactoryOptions; -import lombok.Synchronized; import org.testcontainers.lifecycle.Startable; import org.testcontainers.r2dbc.R2DBCDatabaseContainer; @@ -43,14 +42,12 @@ public Set getDependencies() { } @Override - @Synchronized - public void start() { + public synchronized void start() { this.container.start(); } @Override - @Synchronized - public void stop() { + public synchronized void stop() { this.container.stop(); } diff --git a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java index 90b1f472997..2636c9efe1d 100644 --- a/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java +++ b/modules/selenium/src/main/java/org/testcontainers/containers/BrowserWebDriverContainer.java @@ -5,7 +5,6 @@ import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; import com.google.common.collect.ImmutableSet; -import lombok.Synchronized; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; @@ -352,8 +351,7 @@ public void afterTest(TestDescription description, Optional throwable } @Override - @Synchronized - public void stop() { + public synchronized void stop() { if (driver != null) { try { driver.quit(); diff --git a/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java b/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java index 1de43ef783b..ff11b5df6a3 100644 --- a/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java +++ b/modules/selenium/src/main/java/org/testcontainers/selenium/BrowserWebDriverContainer.java @@ -5,7 +5,6 @@ import com.github.dockerjava.api.model.Bind; import com.github.dockerjava.api.model.Volume; import com.google.common.collect.ImmutableSet; -import lombok.Synchronized; import org.apache.commons.io.FileUtils; import org.apache.commons.lang3.SystemUtils; import org.jetbrains.annotations.NotNull; @@ -203,8 +202,7 @@ public void afterTest(TestDescription description, Optional throwable } @Override - @Synchronized - public void stop() { + public synchronized void stop() { if (vncRecordingContainer != null) { try { vncRecordingContainer.stop();