Skip to content

Commit 94939bb

Browse files
committed
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 <adalwolf@gmail.com>
1 parent 2ac3c97 commit 94939bb

4 files changed

Lines changed: 24 additions & 2 deletions

File tree

modules/kafka/src/main/java/org/testcontainers/containers/KafkaContainer.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import org.testcontainers.utility.ComparableVersion;
77
import org.testcontainers.utility.DockerImageName;
88

9+
import java.io.IOException;
910
import java.util.ArrayList;
1011
import java.util.Arrays;
1112
import java.util.HashSet;
@@ -45,6 +46,8 @@ public class KafkaContainer extends GenericContainer<KafkaContainer> {
4546

4647
private static final String STARTER_SCRIPT = "/tmp/testcontainers_start.sh";
4748

49+
private static final String STARTER_SCRIPT_SENTINEL = STARTER_SCRIPT + ".ready";
50+
4851
// https://docs.confluent.io/platform/7.0.0/release-notes/index.html#ak-raft-kraft
4952
private static final String MIN_KRAFT_TAG = "7.0.0";
5053

@@ -200,6 +203,11 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) {
200203
// Run the original command
201204
command += "/etc/confluent/docker/run \n";
202205
copyFileToContainer(Transferable.of(command, 0777), STARTER_SCRIPT);
206+
try {
207+
execInContainer("touch", STARTER_SCRIPT_SENTINEL);
208+
} catch (IOException | InterruptedException e) {
209+
throw new RuntimeException(e);
210+
}
203211
}
204212

205213
protected String commandKraft() {
@@ -272,7 +280,7 @@ private static class KafkaContainerDef extends ContainerDef {
272280
addExposedTcpPort(KAFKA_PORT);
273281

274282
setEntrypoint("sh");
275-
setCommand("-c", "while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT);
283+
setCommand("-c", "while [ ! -f " + STARTER_SCRIPT_SENTINEL + " ]; do sleep 0.1; done; " + STARTER_SCRIPT);
276284

277285
setWaitStrategy(Wait.forLogMessage(".*\\[KafkaServer id=\\d+\\] started.*", 1));
278286
}

modules/kafka/src/main/java/org/testcontainers/kafka/ConfluentKafkaContainer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.testcontainers.images.builder.Transferable;
66
import org.testcontainers.utility.DockerImageName;
77

8+
import java.io.IOException;
89
import java.util.ArrayList;
910
import java.util.LinkedHashSet;
1011
import java.util.List;
@@ -66,6 +67,11 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) {
6667

6768
command += "/etc/confluent/docker/run \n";
6869
copyFileToContainer(Transferable.of(command, 0777), KafkaHelper.STARTER_SCRIPT);
70+
try {
71+
execInContainer("touch", KafkaHelper.STARTER_SCRIPT_SENTINEL);
72+
} catch (IOException | InterruptedException e) {
73+
throw new RuntimeException(e);
74+
}
6975
}
7076

7177
/**

modules/kafka/src/main/java/org/testcontainers/kafka/KafkaContainer.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import org.testcontainers.images.builder.Transferable;
66
import org.testcontainers.utility.DockerImageName;
77

8+
import java.io.IOException;
89
import java.util.ArrayList;
910
import java.util.LinkedHashSet;
1011
import java.util.List;
@@ -72,6 +73,11 @@ protected void containerIsStarting(InspectContainerResponse containerInfo) {
7273

7374
command += "/etc/kafka/docker/run \n";
7475
copyFileToContainer(Transferable.of(command, 0777), STARTER_SCRIPT);
76+
try {
77+
execInContainer("touch", KafkaHelper.STARTER_SCRIPT_SENTINEL);
78+
} catch (IOException | InterruptedException e) {
79+
throw new RuntimeException(e);
80+
}
7581
}
7682

7783
/**

modules/kafka/src/main/java/org/testcontainers/kafka/KafkaHelper.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ class KafkaHelper {
2525

2626
static final String STARTER_SCRIPT = "/tmp/testcontainers_start.sh";
2727

28+
static final String STARTER_SCRIPT_SENTINEL = STARTER_SCRIPT + ".ready";
29+
2830
static final String[] COMMAND = {
2931
"sh",
3032
"-c",
31-
"while [ ! -f " + STARTER_SCRIPT + " ]; do sleep 0.1; done; " + STARTER_SCRIPT,
33+
"while [ ! -f " + STARTER_SCRIPT_SENTINEL + " ]; do sleep 0.1; done; " + STARTER_SCRIPT,
3234
};
3335

3436
static final WaitStrategy WAIT_STRATEGY = Wait.forLogMessage(".*Transitioning from RECOVERY to RUNNING.*", 1);

0 commit comments

Comments
 (0)