hermes: keep provider lanes available under load
This commit is contained in:
parent
3b291d0e84
commit
e9efec4ddf
@ -1045,14 +1045,15 @@ spec:
|
|||||||
- {name: HERMES_CLAUDE_BROKER_PORT, value: "9006"}
|
- {name: HERMES_CLAUDE_BROKER_PORT, value: "9006"}
|
||||||
- {name: HERMES_CLAUDE_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
|
- {name: HERMES_CLAUDE_BROKER_KEY_FILE, value: /runtime-access/chat-relay-key}
|
||||||
- {name: HERMES_CLAUDE_BROKER_READ_TIMEOUT, value: "1800"}
|
- {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_CLAUDE_HEALTH_PATH, value: /opt/data/provider-health/claude.json}
|
||||||
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
- {name: HERMES_ROUTING_CATALOG_PATH, value: /routing-catalog/catalog.json}
|
||||||
readinessProbe:
|
readinessProbe:
|
||||||
httpGet: {path: /health, port: claude-broker}
|
tcpSocket: {port: claude-broker}
|
||||||
initialDelaySeconds: 5
|
initialDelaySeconds: 5
|
||||||
periodSeconds: 10
|
periodSeconds: 10
|
||||||
livenessProbe:
|
livenessProbe:
|
||||||
httpGet: {path: /health, port: claude-broker}
|
tcpSocket: {port: claude-broker}
|
||||||
initialDelaySeconds: 30
|
initialDelaySeconds: 30
|
||||||
periodSeconds: 30
|
periodSeconds: 30
|
||||||
securityContext:
|
securityContext:
|
||||||
|
|||||||
@ -525,7 +525,13 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Length", str(len(body)))
|
||||||
self.send_header("Cache-Control", "no-store")
|
self.send_header("Cache-Control", "no-store")
|
||||||
self.end_headers()
|
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:
|
def _error(self, status: int, error_type: str, message: str) -> None:
|
||||||
self._json(
|
self._json(
|
||||||
@ -623,7 +629,10 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
self.send_header("Cache-Control", "no-store")
|
self.send_header("Cache-Control", "no-store")
|
||||||
self.send_header("Content-Length", str(len(body)))
|
self.send_header("Content-Length", str(len(body)))
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(body)
|
try:
|
||||||
|
self.wfile.write(body)
|
||||||
|
except (BrokenPipeError, ConnectionResetError):
|
||||||
|
return
|
||||||
else:
|
else:
|
||||||
self._json(200, message)
|
self._json(200, message)
|
||||||
|
|
||||||
|
|||||||
@ -88,6 +88,7 @@ metadata:
|
|||||||
app: hermes-agent
|
app: hermes-agent
|
||||||
spec:
|
spec:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
|
publishNotReadyAddresses: true
|
||||||
selector:
|
selector:
|
||||||
app: hermes-agent
|
app: hermes-agent
|
||||||
ports:
|
ports:
|
||||||
@ -105,6 +106,7 @@ metadata:
|
|||||||
app: hermes-agent
|
app: hermes-agent
|
||||||
spec:
|
spec:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
|
publishNotReadyAddresses: true
|
||||||
selector:
|
selector:
|
||||||
app: hermes-agent
|
app: hermes-agent
|
||||||
ports:
|
ports:
|
||||||
@ -122,6 +124,7 @@ metadata:
|
|||||||
app: hermes-local-image
|
app: hermes-local-image
|
||||||
spec:
|
spec:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
|
publishNotReadyAddresses: true
|
||||||
selector:
|
selector:
|
||||||
app: hermes-local-image
|
app: hermes-local-image
|
||||||
ports:
|
ports:
|
||||||
@ -139,6 +142,7 @@ metadata:
|
|||||||
app: hermes-agent
|
app: hermes-agent
|
||||||
spec:
|
spec:
|
||||||
type: ClusterIP
|
type: ClusterIP
|
||||||
|
publishNotReadyAddresses: true
|
||||||
selector:
|
selector:
|
||||||
app: hermes-agent
|
app: hermes-agent
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import importlib.util
|
import importlib.util
|
||||||
import sys
|
import sys
|
||||||
|
from io import BytesIO
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
|
|
||||||
@ -99,3 +100,22 @@ def test_unadvertised_tool_call_is_retryable(monkeypatch) -> None:
|
|||||||
},
|
},
|
||||||
_payload(),
|
_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})
|
||||||
|
|||||||
@ -47,6 +47,14 @@ def _agent_deployment() -> dict:
|
|||||||
return yaml.safe_load((HERMES / "agent-deployment.yaml").read_text())
|
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:
|
def _oauth_deployment(name: str) -> dict:
|
||||||
documents = [
|
documents = [
|
||||||
item
|
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"]}
|
terminal_env = {item["name"]: item["value"] for item in terminal["env"]}
|
||||||
assert terminal_env["HERMES_TUI_AGENT_INIT_TIMEOUT_S"] == "180"
|
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"]
|
args = containers["oauth2-proxy"]["args"]
|
||||||
terminal_upstream = "--upstream=http://127.0.0.1:7681/terminal/"
|
terminal_upstream = "--upstream=http://127.0.0.1:7681/terminal/"
|
||||||
dashboard_upstream = "--upstream=http://127.0.0.1:9119/"
|
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"
|
] == "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():
|
def test_agent_dashboard_reconnects_all_transient_websockets():
|
||||||
dockerfile = (
|
dockerfile = (
|
||||||
HERMES.parents[1] / "dockerfiles/Dockerfile.hermes-agent"
|
HERMES.parents[1] / "dockerfiles/Dockerfile.hermes-agent"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user