hermes: keep provider lanes available under load

This commit is contained in:
jenkins 2026-08-15 22:15:06 -03:00
parent 3b291d0e84
commit e9efec4ddf
5 changed files with 64 additions and 4 deletions

View File

@ -1045,14 +1045,15 @@ spec:
- {name: HERMES_CLAUDE_BROKER_PORT, value: "9006"}
- {name: HERMES_CLAUDE_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
- {name: HERMES_CLAUDE_BROKER_READ_TIMEOUT, value: "1800"}
- {name: HERMES_CLAUDE_BROKER_CONCURRENCY, value: "2"}
- {name: HERMES_CLAUDE_HEALTH_PATH, value: /opt/data/provider-health/claude.json}
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
readinessProbe:
httpGet: {path: /health, port: claude-broker}
tcpSocket: {port: claude-broker}
initialDelaySeconds: 5
periodSeconds: 10
livenessProbe:
httpGet: {path: /health, port: claude-broker}
tcpSocket: {port: claude-broker}
initialDelaySeconds: 30
periodSeconds: 30
securityContext:

View File

@ -525,7 +525,13 @@ class Handler(BaseHTTPRequestHandler):
self.send_header("Content-Length", str(len(body)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(body)
try:
self.wfile.write(body)
except (BrokenPipeError, ConnectionResetError):
# Kubernetes and callers may close a health/request socket after
# their own deadline. The broker is still healthy and must not
# emit a noisy handler traceback for that normal disconnect.
return
def _error(self, status: int, error_type: str, message: str) -> None:
self._json(
@ -623,7 +629,10 @@ class Handler(BaseHTTPRequestHandler):
self.send_header("Cache-Control", "no-store")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
try:
self.wfile.write(body)
except (BrokenPipeError, ConnectionResetError):
return
else:
self._json(200, message)

View File

@ -88,6 +88,7 @@ metadata:
app: hermes-agent
spec:
type: ClusterIP
publishNotReadyAddresses: true
selector:
app: hermes-agent
ports:
@ -105,6 +106,7 @@ metadata:
app: hermes-agent
spec:
type: ClusterIP
publishNotReadyAddresses: true
selector:
app: hermes-agent
ports:
@ -122,6 +124,7 @@ metadata:
app: hermes-local-image
spec:
type: ClusterIP
publishNotReadyAddresses: true
selector:
app: hermes-local-image
ports:
@ -139,6 +142,7 @@ metadata:
app: hermes-agent
spec:
type: ClusterIP
publishNotReadyAddresses: true
selector:
app: hermes-agent
ports:

View File

@ -4,6 +4,7 @@ from __future__ import annotations
import importlib.util
import sys
from io import BytesIO
from pathlib import Path
from types import ModuleType
@ -99,3 +100,22 @@ def test_unadvertised_tool_call_is_retryable(monkeypatch) -> None:
},
_payload(),
)
@pytest.mark.parametrize("disconnect", [BrokenPipeError, ConnectionResetError])
def test_json_response_tolerates_disconnected_client(monkeypatch, disconnect) -> None:
module = _module(monkeypatch)
handler = object.__new__(module.Handler)
handler.request_version = "HTTP/1.1"
handler.command = "GET"
handler.send_response = lambda _status: None
handler.send_header = lambda _name, _value: None
handler.end_headers = lambda: None
class Disconnected(BytesIO):
def write(self, _body):
raise disconnect
handler.wfile = Disconnected()
handler._json(200, {"ok": True})

View File

@ -47,6 +47,14 @@ def _agent_deployment() -> dict:
return yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
def _services() -> dict[str, dict]:
return {
item["metadata"]["name"]: item
for item in yaml.safe_load_all((HERMES / "service.yaml").read_text())
if item
}
def _oauth_deployment(name: str) -> dict:
documents = [
item
@ -718,6 +726,12 @@ def test_agent_root_is_stock_dashboard_and_terminal_is_a_separate_path():
terminal_env = {item["name"]: item["value"] for item in terminal["env"]}
assert terminal_env["HERMES_TUI_AGENT_INIT_TIMEOUT_S"] == "180"
claude_broker = containers["claude-broker"]
claude_env = {item["name"]: item["value"] for item in claude_broker["env"]}
assert claude_env["HERMES_CLAUDE_BROKER_CONCURRENCY"] == "2"
assert claude_broker["readinessProbe"]["tcpSocket"] == {"port": "claude-broker"}
assert claude_broker["livenessProbe"]["tcpSocket"] == {"port": "claude-broker"}
args = containers["oauth2-proxy"]["args"]
terminal_upstream = "--upstream=http://127.0.0.1:7681/terminal/"
dashboard_upstream = "--upstream=http://127.0.0.1:9119/"
@ -774,6 +788,18 @@ def test_agent_root_is_stock_dashboard_and_terminal_is_a_separate_path():
] == "hermes-hermes-agent-terminal-slash@kubernetescrd"
def test_broker_services_survive_sibling_container_readiness_loss():
services = _services()
for name in (
"hermes-image-broker",
"hermes-codex-broker",
"hermes-local-image",
"hermes-claude-broker",
):
assert services[name]["spec"]["publishNotReadyAddresses"] is True
def test_agent_dashboard_reconnects_all_transient_websockets():
dockerfile = (
HERMES.parents[1] / "dockerfiles/Dockerfile.hermes-agent"