fix(docker): read upgraded exec streams through the buffered reader - #15984
Conversation
❌ Test Summary: FAILED✅ Precommit: PASSED
❌ Tests: FAILED
Failed Tests
|
remote_file() passed the 'mktemp' result to send_files() without checking it. An empty result made every write fail, and wait.wait_for() then retried it for 300s before the run ended as NodeSetupFailed. It now raises at once and names the real problem. send_files() also built the extraction directory with pathlib.Path, which reads a path with the rules of the host. A path inside a container is POSIX whatever the host runs, so it uses PurePosixPath now.
A docker exec can return empty output with exit code 0. The daemon sends the '101 UPGRADED' headers and the first output frame together. http.client keeps that frame in its read buffer. docker-py skips the buffer and reads from the socket, so it never sees the frame. SCT then uses the empty output as a real value. One effect is that an empty 'mktemp' result can become the path in container.put_archive(path=""). The daemon then refuses writes with '400 ... path cannot be empty' until the 300s wait ends as NodeSetupFailed. This is a known upstream docker/docker-py#3332 issue. The change fixes this by adding BufferedStreamAPIClient that gives docker-py frame reader the buffer instead of the socket, and docker.utils.socket.read reads a buffer without polling the socket first. Without that skipped poll a buffered line waits for the next one.
f93da60 to
2b465e4
Compare
|
@dimakr the docker and integration tests fails on known issue I'll try getting the docker py issue fixed (with clearer PR for it) |
What
A docker exec can return empty output with exit code 0, and SCT then uses that empty output as a real value. One effect is that an empty
mktempresult becomes the path incontainer.put_archive(path=""). The daemon refuses every write with400 Bad Request: bad parameter: path cannot be empty, the 300 s retry budget insdcm/wait.pyruns out, and the run fails withNodeSetupFailedwhile SCT writesscylla.yamlinto the container. Seeartifacts-docker-fips-testbuild 496.Root cause
The daemon answers an exec start with
101 UPGRADEDheaders and then writes the output frames on the same connection. http.client parses those headers through a BufferedReader that reads upto 8 KiB at a time, so a frame arriving in the same
recv()lands in that buffer. docker-py skips the buffer and reads from the socket, so it never sees the frame.This is docker/docker-py#3332, also reported as #2042. The upstream fix is docker/docker-py#3333, open and unreviewed since May 2025, and docker-py 7.2.0 still carries the old code. This change mirrors it, so it can be dropped in one piece once a release includes it.
It is not a recent regression, and nothing is specific to
mktemp. AnyDockerCmdRunner.run()call whose output SCT parses can hit it, and so can the otherexec_run()callers, such as the gcloud container insdcm/utils/gce_utils.py. The window is small, so the failure is rare, and a busy runner widens it.Fix
sdcm/utils/docker_utils.py:BufferedStreamAPIClientgives docker-py's frame reader the buffer instead of the socket, anddocker.utils.socket.readreads a buffer without pollingthe socket first. docker-py still does the framing, the demux and the response close. SCT's
DockerClientbuilds its api object from the subclass, so every container SCT talks to iscovered. The ssh and named-pipe transports keep something else in that attribute and still go through docker-py.
sdcm/remote/remote_file.py: raise when themktempresult is empty, instead of retrying an unusable destination for 300 s. This also covers the ssh transport, where the change abovedeliberately leaves docker-py in charge.
sdcm/remote/docker_cmd_runner.py: build the extraction directory withPurePosixPath. A path inside a container is POSIX whatever the host runs.Fixes SCT-952
Testing