fix(ccproxy): raise startup timeouts (auth check 10s→30s, serve health 30s→180s) (#328)
* fix(ccproxy): raise auth status check timeout to 30s ccproxy's CLI initializes its full plugin system on every invocation; a cold 'ccproxy auth status' takes ~10s wall time on Apple Silicon, so the 10s subprocess timeout made OAuth startup fail intermittently with 'Auth check timed out' even when credentials were valid. * fix(ccproxy): raise serve health deadline to 120s ccproxy boot includes plugin init plus Codex CLI detection; measured ~76s to first healthy response on an Apple Silicon Mac (ccproxy-api 0.2.9). The 30s deadline in start_ccproxy() killed the process before it could come up, failing OAuth startup with 'ccproxy did not become healthy within 30 seconds'. * fix(ccproxy): widen serve health deadline to 180s Full startup measured at ~111s on a second cold run (Apple Silicon, ccproxy-api 0.2.9); 120s left too little headroom for boot variance. * fix(ccproxy): centralize startup timeouts --------- Co-authored-by: Xi Zhang <106144707+X-iZhang@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
f3e65a446f
commit
19888c2db6
@@ -20,6 +20,9 @@ from EvoScientist.config import EvoScientistConfig
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CCPROXY_AUTH_TIMEOUT_SECONDS = 30
|
||||
_CCPROXY_HEALTH_TIMEOUT_SECONDS = 180
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Availability & auth checks
|
||||
@@ -127,7 +130,11 @@ def check_ccproxy_auth(provider: str = "claude_api") -> tuple[bool, str]:
|
||||
[exe, "auth", "status", provider],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=10,
|
||||
# ccproxy's CLI initializes its full plugin system on every
|
||||
# invocation — a cold start takes ~10s on Apple Silicon, so a
|
||||
# 10s timeout made OAuth startup fail intermittently with
|
||||
# "Auth check timed out".
|
||||
timeout=_CCPROXY_AUTH_TIMEOUT_SECONDS,
|
||||
)
|
||||
import re as _re
|
||||
|
||||
@@ -186,18 +193,23 @@ def start_ccproxy(port: int) -> subprocess.Popen:
|
||||
The Popen handle for the ccproxy process.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If ccproxy fails to become healthy within 30 seconds.
|
||||
RuntimeError: If ccproxy fails to become healthy within
|
||||
``_CCPROXY_HEALTH_TIMEOUT_SECONDS``.
|
||||
FileNotFoundError: If ccproxy binary is not found.
|
||||
"""
|
||||
exe = _ccproxy_exe() or "ccproxy"
|
||||
logger.warning(
|
||||
"Starting ccproxy on port %d; first startup may take up to %d seconds",
|
||||
port,
|
||||
_CCPROXY_HEALTH_TIMEOUT_SECONDS,
|
||||
)
|
||||
proc = subprocess.Popen(
|
||||
[exe, "serve", "--port", str(port)],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
# Wait for health (ccproxy can take up to ~11s on first start)
|
||||
deadline = time.monotonic() + 30
|
||||
deadline = time.monotonic() + _CCPROXY_HEALTH_TIMEOUT_SECONDS
|
||||
while time.monotonic() < deadline:
|
||||
if proc.poll() is not None:
|
||||
raise RuntimeError(
|
||||
@@ -213,7 +225,10 @@ def start_ccproxy(port: int) -> subprocess.Popen:
|
||||
proc.wait(timeout=3)
|
||||
except subprocess.TimeoutExpired:
|
||||
proc.kill()
|
||||
raise RuntimeError("ccproxy did not become healthy within 30 seconds")
|
||||
raise RuntimeError(
|
||||
"ccproxy did not become healthy within "
|
||||
f"{_CCPROXY_HEALTH_TIMEOUT_SECONDS} seconds"
|
||||
)
|
||||
|
||||
|
||||
def stop_ccproxy(proc: subprocess.Popen | None) -> None:
|
||||
|
||||
@@ -6,6 +6,8 @@ from unittest.mock import MagicMock, patch
|
||||
import pytest
|
||||
|
||||
from EvoScientist.ccproxy_manager import (
|
||||
_CCPROXY_AUTH_TIMEOUT_SECONDS,
|
||||
_CCPROXY_HEALTH_TIMEOUT_SECONDS,
|
||||
check_ccproxy_auth,
|
||||
ensure_ccproxy,
|
||||
is_ccproxy_available,
|
||||
@@ -52,6 +54,8 @@ class TestCheckCcproxyAuth:
|
||||
mock_run.assert_called_once()
|
||||
cmd = mock_run.call_args[0][0]
|
||||
assert cmd[1:] == ["auth", "status", "claude_api"]
|
||||
# ccproxy CLI cold start takes ~10s; timeout must leave headroom
|
||||
assert mock_run.call_args[1]["timeout"] == _CCPROXY_AUTH_TIMEOUT_SECONDS
|
||||
|
||||
@patch("subprocess.run")
|
||||
def test_valid_auth_codex(self, mock_run):
|
||||
@@ -123,9 +127,10 @@ class TestIsCcproxyRunning:
|
||||
|
||||
|
||||
class TestStartCcproxy:
|
||||
@patch("EvoScientist.ccproxy_manager.logger.warning")
|
||||
@patch("EvoScientist.ccproxy_manager.is_ccproxy_running")
|
||||
@patch("subprocess.Popen")
|
||||
def test_success(self, mock_popen, mock_running):
|
||||
def test_success(self, mock_popen, mock_running, mock_warning):
|
||||
proc = MagicMock()
|
||||
proc.poll.return_value = None
|
||||
mock_popen.return_value = proc
|
||||
@@ -134,6 +139,11 @@ class TestStartCcproxy:
|
||||
|
||||
result = start_ccproxy(8000)
|
||||
assert result is proc
|
||||
mock_warning.assert_called_once_with(
|
||||
"Starting ccproxy on port %d; first startup may take up to %d seconds",
|
||||
8000,
|
||||
_CCPROXY_HEALTH_TIMEOUT_SECONDS,
|
||||
)
|
||||
|
||||
@patch("EvoScientist.ccproxy_manager.is_ccproxy_running", return_value=False)
|
||||
@patch("EvoScientist.ccproxy_manager.time")
|
||||
@@ -143,7 +153,11 @@ class TestStartCcproxy:
|
||||
proc.poll.return_value = None
|
||||
mock_popen.return_value = proc
|
||||
# Simulate time passing beyond deadline
|
||||
mock_time.monotonic.side_effect = [0, 0, 31]
|
||||
mock_time.monotonic.side_effect = [
|
||||
0,
|
||||
0,
|
||||
_CCPROXY_HEALTH_TIMEOUT_SECONDS + 1,
|
||||
]
|
||||
mock_time.sleep = MagicMock()
|
||||
|
||||
with pytest.raises(RuntimeError, match="did not become healthy"):
|
||||
|
||||
Reference in New Issue
Block a user