Skip to content

[Bug]: DockerClientProviderStrategy.test() reports a busy Windows named pipe as absent, so parallel test JVMs intermittently find no Docker environment #12128

Open

Description

@mertakdut

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:        de40ad0

What 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 configuration

Additional Information

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions