Stop long prunes failing with a bare 500 (0.60.1)
A prune that deletes a lot of data (image prune -a in particular) easily runs past docker-py's 60s read timeout. requests' ReadTimeout is not a DockerException, so it slipped past safe_call and the UI only showed "Request failed with status code 500". Prunes now use a client with a 540s timeout (below nginx's 600s proxy_read_timeout), and safe_call turns transport timeouts and connection errors into DockerError, so they surface as a 502 with a message. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8edea8f971
commit
f766ded528
7 files changed
+120
-11
No files matched your search
@@ -0,0 +1,81 @@
|
||||
"""Slow Docker calls must fail with a message, not a bare 500.
|
||||
|
||||
A volume or image prune that deletes a lot of data easily runs past docker-py's
|
||||
60s read timeout. requests' ``ReadTimeout`` is not a ``DockerException``, so it
|
||||
slipped past ``safe_call`` and the UI only showed "Request failed with status
|
||||
code 500". Prunes now use a client with a long timeout, and any transport
|
||||
timeout that still happens is turned into a ``DockerError`` (-> 502 + detail).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
|
||||
def test_safe_call_wraps_read_timeout():
|
||||
from docker_client import DockerError, safe_call
|
||||
|
||||
def slow():
|
||||
raise requests.exceptions.ReadTimeout("Read timed out. (read timeout=60)")
|
||||
|
||||
with pytest.raises(DockerError) as info:
|
||||
safe_call(slow)
|
||||
assert info.value.error == "docker_timeout"
|
||||
|
||||
|
||||
def test_safe_call_wraps_connection_error():
|
||||
from docker_client import DockerError, safe_call
|
||||
|
||||
def gone():
|
||||
raise requests.exceptions.ConnectionError("socket went away")
|
||||
|
||||
with pytest.raises(DockerError) as info:
|
||||
safe_call(gone)
|
||||
assert info.value.error == "docker_unavailable"
|
||||
|
||||
|
||||
def test_volume_prune_timeout_is_a_502_with_detail(as_admin, monkeypatch):
|
||||
from services import volume_service
|
||||
|
||||
class Volumes:
|
||||
def prune(self, filters=None):
|
||||
raise requests.exceptions.ReadTimeout("Read timed out.")
|
||||
|
||||
class Client:
|
||||
volumes = Volumes()
|
||||
|
||||
monkeypatch.setattr(volume_service, "get_long_client", lambda: Client())
|
||||
r = as_admin.post("/api/volumes/prune")
|
||||
assert r.status_code == 502
|
||||
assert r.json()["error"] == "docker_timeout"
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"module, fn, args",
|
||||
[
|
||||
("volume_service", "prune_volumes", ()),
|
||||
("image_service", "prune_images", (True,)),
|
||||
("network_service", "prune_networks", ()),
|
||||
],
|
||||
)
|
||||
def test_prunes_use_the_long_timeout_client(monkeypatch, module, fn, args):
|
||||
import importlib
|
||||
|
||||
svc = importlib.import_module(f"services.{module}")
|
||||
used = []
|
||||
|
||||
class Any_:
|
||||
def prune(self, filters=None):
|
||||
used.append("long")
|
||||
return {}
|
||||
|
||||
class Client:
|
||||
volumes = images = networks = Any_()
|
||||
|
||||
def short():
|
||||
raise AssertionError("prune must not use the 60s client")
|
||||
|
||||
monkeypatch.setattr(svc, "get_client", short)
|
||||
monkeypatch.setattr(svc, "get_long_client", lambda: Client())
|
||||
getattr(svc, fn)(*args)
|
||||
assert used == ["long"]
|
||||
Reference in new issue
Block a user