Disable timeout for Docker image pull operations (#6391)

* Disable timeout for Docker image pull operations

The aiodocker migration introduced a regression where image pulls could
timeout during slow downloads. The session-level timeout (900s total)
was being applied to pull operations, but docker-py explicitly sets
timeout=None for pulls, allowing them to run indefinitely.

When aiodocker receives timeout=None, it converts it to
ClientTimeout(total=None), which aiohttp treats as "no timeout"
(returns TimerNoop instead of enforcing a timeout).

This fixes TimeoutError exceptions that could occur during installation
on systems with slow network connections or when pulling large images.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Fix pytests

---------

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Stefan Agner 2025-12-03 21:52:46 +01:00 committed by GitHub
parent 3b3db2a9bc
commit 382f0e8aef
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 28 additions and 6 deletions

View File

@ -474,8 +474,10 @@ class DockerAPI(CoreSysAttributes):
raises only if the get fails afterwards. Additionally it fires progress reports for the pull raises only if the get fails afterwards. Additionally it fires progress reports for the pull
on the bus so listeners can use that to update status for users. on the bus so listeners can use that to update status for users.
""" """
# Use timeout=None to disable timeout for pull operations, matching docker-py behavior.
# aiodocker converts None to ClientTimeout(total=None) which disables the timeout.
async for e in self.images.pull( async for e in self.images.pull(
repository, tag=tag, platform=platform, auth=auth, stream=True repository, tag=tag, platform=platform, auth=auth, stream=True, timeout=None
): ):
entry = PullLogEntry.from_pull_log_dict(job_id, e) entry = PullLogEntry.from_pull_log_dict(job_id, e)
if entry.error: if entry.error:

View File

@ -54,7 +54,7 @@ async def test_docker_image_platform(
coresys.docker.images.inspect.return_value = {"Id": "test:1.2.3"} coresys.docker.images.inspect.return_value = {"Id": "test:1.2.3"}
await test_docker_interface.install(AwesomeVersion("1.2.3"), "test", arch=cpu_arch) await test_docker_interface.install(AwesomeVersion("1.2.3"), "test", arch=cpu_arch)
coresys.docker.images.pull.assert_called_once_with( coresys.docker.images.pull.assert_called_once_with(
"test", tag="1.2.3", platform=platform, auth=None, stream=True "test", tag="1.2.3", platform=platform, auth=None, stream=True, timeout=None
) )
coresys.docker.images.inspect.assert_called_once_with("test:1.2.3") coresys.docker.images.inspect.assert_called_once_with("test:1.2.3")
@ -71,7 +71,12 @@ async def test_docker_image_default_platform(
): ):
await test_docker_interface.install(AwesomeVersion("1.2.3"), "test") await test_docker_interface.install(AwesomeVersion("1.2.3"), "test")
coresys.docker.images.pull.assert_called_once_with( coresys.docker.images.pull.assert_called_once_with(
"test", tag="1.2.3", platform="linux/386", auth=None, stream=True "test",
tag="1.2.3",
platform="linux/386",
auth=None,
stream=True,
timeout=None,
) )
coresys.docker.images.inspect.assert_called_once_with("test:1.2.3") coresys.docker.images.inspect.assert_called_once_with("test:1.2.3")
@ -111,7 +116,12 @@ async def test_private_registry_credentials_passed_to_pull(
expected_auth["registry"] = registry_key expected_auth["registry"] = registry_key
coresys.docker.images.pull.assert_called_once_with( coresys.docker.images.pull.assert_called_once_with(
image, tag="1.2.3", platform="linux/amd64", auth=expected_auth, stream=True image,
tag="1.2.3",
platform="linux/amd64",
auth=expected_auth,
stream=True,
timeout=None,
) )
@ -360,7 +370,12 @@ async def test_install_fires_progress_events(
): ):
await test_docker_interface.install(AwesomeVersion("1.2.3"), "test") await test_docker_interface.install(AwesomeVersion("1.2.3"), "test")
coresys.docker.images.pull.assert_called_once_with( coresys.docker.images.pull.assert_called_once_with(
"test", tag="1.2.3", platform="linux/386", auth=None, stream=True "test",
tag="1.2.3",
platform="linux/386",
auth=None,
stream=True,
timeout=None,
) )
coresys.docker.images.inspect.assert_called_once_with("test:1.2.3") coresys.docker.images.inspect.assert_called_once_with("test:1.2.3")
@ -817,7 +832,12 @@ async def test_install_progress_containerd_snapshot(
with patch.object(Supervisor, "arch", PropertyMock(return_value="i386")): with patch.object(Supervisor, "arch", PropertyMock(return_value="i386")):
await test_docker_interface.mock_install() await test_docker_interface.mock_install()
coresys.docker.images.pull.assert_called_once_with( coresys.docker.images.pull.assert_called_once_with(
"test", tag="1.2.3", platform="linux/386", auth=None, stream=True "test",
tag="1.2.3",
platform="linux/386",
auth=None,
stream=True,
timeout=None,
) )
coresys.docker.images.inspect.assert_called_once_with("test:1.2.3") coresys.docker.images.inspect.assert_called_once_with("test:1.2.3")