feat(api)!: add a server drain and never answer with a 5xx

POST/DELETE /api/dashboard/drain refuses new sessions and jobs (SERVER_DRAINING, 409) and warns open sessions. A CDN can replace a 5xx body, so server errors are now 422, retryable ones and the tunnel check 424, and a new middleware turns any 5xx into 422.

BREAKING CHANGE: INTERNAL_ERROR, DOWNLOAD_ERROR, WORKER_ERROR and the fallback return 422 instead of 500; NETWORK_ERROR and SERVICE_UNAVAILABLE return 424 instead of 503; /api/tunnel/check failures return 424 instead of 502.
This commit is contained in:
imSp4rky
2026-10-21 17:15:33 -06:00
parent 23614fd938
commit 5086dafff3
14 changed files with 264 additions and 71 deletions
@@ -558,11 +558,12 @@ def test_session_create_writes_nested_cache_and_skips_colliding_keys(tmp_path, m
"create_service_instance",
lambda *args, **kwargs: (SimpleNamespace(log=logging.getLogger("Svc")), None, None),
)
monkeypatch.setattr("unshackle.core.api.session_store.refuse_while_draining", lambda: None)
monkeypatch.setattr("unshackle.core.api.session_store.get_session_store", lambda: (_ for _ in ()).throw(Stop()))
cache = {"x": blob('"flat"'), "x.json/y": blob('"collides"'), "MSL/keys.v2": blob('"dotted"')}
response = asyncio.run(handlers.session_create_handler({"service": "TESTSVC", "title_id": "t", "cache": cache}))
assert response.status == 500 # the Stop stand-in fired after the cache write
assert response.status == 422 # the Stop stand-in fired after the cache write
written = {p.name: p.read_text() for p in tmp_path.rglob("*.json") if p.is_file()}
assert written == {"x.json": '"flat"', "keys.v2.json": '"dotted"'}