Repository navigation
[Bug]: DockerClientProviderStrategy.test() reports a busy Windows named pipe as absent, so parallel test JVMs intermittently find no Docker environment #12128
Description
Module
Core
Testcontainers version
2.0.5
Using the latest Testcontainers version?
Yes
Host OS
Windows (Windows 11 Pro 10.0.26300)
Host Arch
x86-64
Docker version
Client:
Version: 29.9.2
API version: 1.56
Go version: go1.26.10
Git commit: a82e528
Built: Tue Oct 20 19:44:07 2026
OS/Arch: windows/amd64
Context: desktop-linux
Server: Docker Desktop 4.96.3 (244472)
Engine:
Version: 29.9.2
API version: 1.56 (minimum version 1.40)
Go version: go1.26.10
Git commit: 0367cbe
Built: Tue Oct 20 18:45:02 2026
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: v2.4.2
GitCommit: b7a25261e029d68978bbf290df0a62fc819b3c51
runc:
Version: 1.5.2
GitCommit: v1.5.2-0-g29dd3dc2
docker-init:
Version: 0.19.0
GitCommit: de40ad0What happened?
A Gradle test task runs with four forks (OpenJDK 26.0.2.1, Gradle 9.8.2), each starting its own containers. ~/.testcontainers.properties names NpipeSocketClientProviderStrategy. Now and then one fork's first container-backed class fails with "Could not find a valid Docker environment. Please check configuration. Attempted configurations were:" followed by nothing, and every later class in that fork fails with "Previous attempts to find a Docker environment failed. Will not retry". The other forks pass, Docker Desktop is running, and a re-run passes. Two of three full runs at 16 forks lost a fork. The chance of the wrong answer grows with the number of simultaneous callers (measurements below).
Expected: a busy pipe is waited on, as the connection attempt that follows the check already does.
Mechanism. For the unix and npipe schemes, DockerClientProviderStrategy.test() (2.0.5, lines 186-189; unchanged on main) returns false when new File(dockerHost.getPath()).exists() is false, before any connection attempt. On Windows the JDK answers File.exists() with GetFileAttributesExW and falls back to FindFirstFileW only for ERROR_SHARING_VIOLATION and ERROR_ACCESS_DENIED (WinNTFileSystem_md.c, getFinalAttributes). On a named pipe, an attribute query is a client open: it takes a listening instance of the pipe, and when every instance is taken it fails with ERROR_PIPE_BUSY (231), which the JDK maps to "does not exist". The instances are taken by the other forks' Docker traffic and by their own existence checks. A false from test() is logged at debug level only and appends nothing to the failure list (tryOutStrategy, lines 284-285), which is why the message lists no attempted configuration; with no other applicable strategy on Windows the JVM sets FAIL_FAST_ALWAYS (line 273).
The connection attempt that follows the check already tolerates a busy pipe: docker-java's NamedPipeSocket.connect (docker-java-transport 3.7.1, lines 47-68) loops on WaitNamedPipe for any FileSystemException for at least ten seconds, the behaviour docker-java#1418 introduced, and the Docker CLI does the same. The existence check is the only step that cannot tolerate a busy pipe.
Measurements, 50 rounds each, one JVM, simultaneous callers released by a latch:
| Simultaneous callers | File.exists("//./pipe/docker_engine") false |
GetFileAttributesExW on the same path failed |
Error code |
|---|---|---|---|
| 1 | 0 % | 0 % | none |
| 4 | 21 % | 41 % | 231 in every case |
| 8 | 41 % | 40 % | 231 in every case |
| 16 | 50 % | 40 % | 231 in every case |
Deterministic reproduction, no race needed. Create a named pipe with CreateNamedPipeW and a maximum of one instance, connect one client to it and hold the handle. While it is held, File.exists() on the pipe path is false, AsynchronousFileChannel.open throws FileSystemException "All pipe instances are busy", and NamedPipeSocket.connect waits and succeeds the moment the holder releases the instance. With the holder kept beyond the wait, connect throws RuntimeException caused by that FileSystemException after 10.1 s. Against a pipe that does not exist, AsynchronousFileChannel.open throws NoSuchFileException and connect throws after 10.0 s.
Proposed change. For the npipe scheme, drop the existence pre-check and let the connection attempt decide: NamedPipeSocket.connect already waits on a busy pipe and surfaces an absent one as NoSuchFileException. Trade-off: an absent pipe is detected after ten seconds (the floor NamedPipeSocket.connect puts on its own wait when called without a timeout, lines 42-49), or after the client ping timeout when that is lower, since the Awaitility wait around the connect (lines 208-214) ends it, instead of at once. Since test() catches every connection exception, logs it at warn level and returns false (lines 216-219), the failure list stays empty as today; recording the reason there would mean that catch, shared by the tcp, unix and npipe branches, also appends to the list, a second change to weigh. Pull request with the change and a Windows test: #12129.
Relevant log output
Could not find a valid Docker environment. Please check configuration. Attempted configurations were:
As no valid configuration was found, execution cannot continue.
See https://java.testcontainers.org/on_failure.html for more details.
java.lang.IllegalStateException: Previous attempts to find a Docker environment failed. Will not retry. Please see logs and check configurationAdditional Information
FAIL_FAST_ALWAYSfor selection ofDockerClientProviderStrategynot reset in flaky environments when tests are retried #5017 carries a 2026-03-19 comment describing the same symptom on Windows 11 with Docker Desktop and parallel tests, "no further information is printed into log", root cause unknown, worked around by resettingFAIL_FAST_ALWAYSthrough reflection.- Using File.Exists to check the pipe created will make the NamedPipeClientStream connect fail dotnet/runtime#69604 records the same Windows mechanism for .NET's
File.Exists: a Microsoft maintainer states that the named pipe file system supports no attribute query without an open, and names enumeration orWaitNamedPipeas the only alternatives. - Alternative to the proposed change: enumerate the pipe directory, which
FindFirstFileWdoes without opening the pipe, instead of querying attributes. - A further option in docker-java: stop
NamedPipeSocket.connectfrom waiting onNoSuchFileException, so an absent pipe fails fast again.
Activity