From 94939bba2d7afc90fb56128b91c64dae815f897b Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 3 Aug 2026 16:13:17 +0200 Subject: [PATCH] fix(kafka): use sentinel file to eliminate starter-script race condition (#11682) copyFileToContainer writes the startup script into the container over a Docker API call. On busy hosts the wait loop can detect the file the moment its inode is created, before the write completes, and attempt to execute an incomplete file. This produces ETXTBSY and exit code 126. The fix adds a sentinel file (STARTER_SCRIPT + ".ready") that is touched via execInContainer only after copyFileToContainer returns. The wait loop now checks for the sentinel instead of the script itself, so execution cannot begin until the script is fully written. Applied to KafkaHelper, org.testcontainers.kafka.KafkaContainer, org.testcontainers.kafka.ConfluentKafkaContainer, and the deprecated org.testcontainers.containers.KafkaContainer. Signed-off-by: klouds27 --- .../org/testcontainers/containers/KafkaContainer.java | 10 +++++++++- .../testcontainers/kafka/ConfluentKafkaContainer.java | 6 ++++++ .../java/org/testcontainers/kafka/KafkaContainer.java | 6 ++++++ .../java/org/testcontainers/kafka/KafkaHelper.java | 4 +++- 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java b/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java index 7eb836ade18..aa04d877186 100644 --- a/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java +++ b/modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java @@ -6,6 +6,7 @@ import org.testcontainers.utility.ComparableVersion; import org.testcontainers.utility.DockerImageName; +import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; @@ -45,6 +46,8 @@ public class KafkaContainer extends GenericContainer { private static final String STARTER_SCRIPT = "/tmp/testcontainers_start.sh"; + private static final String STARTER_SCRIPT_SENTINEL = STARTER_SCRIPT + ".ready"; + // https://docs.confluent.io/platform/7.0.0/release-notes/index.html#ak-raft-kraft private static final String MIN_KRAFT_TAG = "7.0.0"; @@ -200,6 +203,11 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) { // Run the original command command += "/etc/confluent/docker/run \n"; copyFileToContainer(Transferable.of(command, 0777), STARTER_SCRIPT); + try { + execInContainer("touch", STARTER_SCRIPT_SENTINEL); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } } protected String commandKraft() { @@ -272,7 +280,7 @@ private static class KafkaContainerDef extends ContainerDef { addExposedTcpPort(KAFKA_PORT); setEntrypoint("sh"); - setCommand("-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT); + setCommand("-c", "while [ ! -f " + STARTER_SCRIPT_SENTINEL + " ]; do sleep 0.1; done; " + STARTER_SCRIPT); setWaitStrategy(Wait.forLogMessage(".*\\[KafkaServer id=\\d+\\] started.*", 1)); } diff --git a/modules/kafka/src/main/java/org/testcontainers/kafka/ConfluentKafkaContainer.java b/modules/kafka/src/main/java/org/testcontainers/kafka/ConfluentKafkaContainer.java index 381ba836715..7697c4934cd 100644 --- a/modules/kafka/src/main/java/org/testcontainers/kafka/ConfluentKafkaContainer.java +++ b/modules/kafka/src/main/java/org/testcontainers/kafka/ConfluentKafkaContainer.java @@ -5,6 +5,7 @@ import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; +import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @@ -66,6 +67,11 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) { command += "/etc/confluent/docker/run \n"; copyFileToContainer(Transferable.of(command, 0777), KafkaHelper.STARTER_SCRIPT); + try { + execInContainer("touch", KafkaHelper.STARTER_SCRIPT_SENTINEL); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } } /** diff --git a/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaContainer.java b/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaContainer.java index 375fd132f6c..00ed8cfe688 100644 --- a/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaContainer.java +++ b/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaContainer.java @@ -5,6 +5,7 @@ import org.testcontainers.images.builder.Transferable; import org.testcontainers.utility.DockerImageName; +import java.io.IOException; import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @@ -72,6 +73,11 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) { command += "/etc/kafka/docker/run \n"; copyFileToContainer(Transferable.of(command, 0777), STARTER_SCRIPT); + try { + execInContainer("touch", KafkaHelper.STARTER_SCRIPT_SENTINEL); + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } } /** diff --git a/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaHelper.java b/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaHelper.java index 61e790d474f..12a8a6eb47c 100644 --- a/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaHelper.java +++ b/modules/kafka/src/main/java/org/testcontainers/kafka/KafkaHelper.java @@ -25,10 +25,12 @@ class KafkaHelper { static final String STARTER_SCRIPT = "/tmp/testcontainers_start.sh"; + static final String STARTER_SCRIPT_SENTINEL = STARTER_SCRIPT + ".ready"; + static final String[] COMMAND = { "sh", "-c", - "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT, + "while [ ! -f " + STARTER_SCRIPT_SENTINEL + " ]; do sleep 0.1; done; " + STARTER_SCRIPT, }; static final WaitStrategy WAIT_STRATEGY = Wait.forLogMessage(".*Transitioning from RECOVERY to RUNNING.*", 1);