Skip to content

Commit 2e5540a

Browse files
MervinPraisonpraisonai-triage-agent[bot]Copilot
authored
fix: check agent server readiness timeout return value (fixes #1631) (#1655)
* fix: check agent server readiness timeout return value (fixes #1631) - Add return value checking for ready_event.wait() to detect timeouts - Log warning when server doesn't become ready within timeout period - Make timeout configurable via PRAISONAI_SERVER_READY_TIMEOUT env var - Add comprehensive regression tests for timeout behavior Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com> * fix: address reviewer feedback - add error handling for invalid timeout env var - Add try/catch for float() conversion to prevent ValueError crashes - Prevent real server spawning in unit tests via Thread.start mocking - Add test for invalid env var fallback behavior - Preserve other env vars during testing (avoid clear=True) Addresses feedback from Gemini, CodeRabbit, Qodo, and Copilot reviewers. Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com> * fix: use monkeypatch.delenv and move import time to module level - Replace patch.dict(os.environ, {"": ""}) in test_default_timeout_value with monkeypatch.delenv so the test exercises the true "env var unset" default path rather than the ValueError fallback path - Move `import time` from inside test function body to module-level imports Agent-Logs-Url: https://github.com/MervinPraison/PraisonAI/sessions/5c472f85-484f-4a03-b47a-9307639ecd0a Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com> --------- Co-authored-by: praisonai-triage-agent[bot] <272766704+praisonai-triage-agent[bot]@users.noreply.github.com> Co-authored-by: praisonai-triage-agent[bot] <praisonai-triage-agent[bot]@users.noreply.github.com> Co-authored-by: Mervin Praison <MervinPraison@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: MervinPraison <454862+MervinPraison@users.noreply.github.com>
1 parent 7fcd6af commit 2e5540a

2 files changed

Lines changed: 146 additions & 1 deletion

File tree

src/praisonai-agents/praisonaiagents/agents/agents.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,24 @@ def run_server():
126126

127127
thread = threading.Thread(target=run_server, daemon=True)
128128
thread.start()
129-
ready_event.wait(timeout=5.0) # Deterministic wait instead of sleep(0.5)
129+
130+
# Check for configurable timeout via environment variable
131+
try:
132+
timeout = float(os.environ.get("PRAISONAI_SERVER_READY_TIMEOUT", "5.0"))
133+
except ValueError:
134+
logger.warning("Invalid PRAISONAI_SERVER_READY_TIMEOUT value. Using default 5.0s.")
135+
timeout = 5.0
136+
became_ready = ready_event.wait(timeout=timeout)
137+
138+
if not became_ready:
139+
logger.warning(
140+
"Agent server on port %s did not become ready within %.1fs. "
141+
"Proceeding, but some features may not work correctly. "
142+
"Check server logs for startup errors.",
143+
port,
144+
timeout,
145+
)
146+
130147
return True
131148

132149

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
"""Test for agent server readiness timeout warning functionality."""
2+
3+
import time
4+
import threading
5+
import logging
6+
import pytest
7+
from unittest.mock import patch, MagicMock
8+
from praisonaiagents.agents.agents import _AgentServerRegistry
9+
10+
11+
def test_server_readiness_timeout_emits_warning(caplog):
12+
"""A server that does not start in time must produce a WARNING log entry."""
13+
registry = _AgentServerRegistry()
14+
15+
# Create a mock FastAPI app that won't signal readiness
16+
mock_app = MagicMock()
17+
18+
# Register the app but don't set the ready event
19+
with patch.dict(registry._apps, {8000: mock_app}):
20+
with patch.dict(registry._ready_events, {8000: threading.Event()}):
21+
with patch.dict(registry._started, {8000: False}):
22+
with patch("threading.Thread.start"): # Prevent real server spawning
23+
with caplog.at_level(logging.WARNING):
24+
# Use a very short timeout to ensure the test doesn't hang
25+
with patch.dict("os.environ", {"PRAISONAI_SERVER_READY_TIMEOUT": "0.1"}):
26+
result = registry.start_server_if_needed(8000, host="127.0.0.1")
27+
28+
# The method should still return True but log a warning
29+
assert result is True
30+
assert "8000" in caplog.text
31+
assert "did not become ready" in caplog.text.lower()
32+
33+
34+
def test_server_readiness_timeout_configurable_via_env():
35+
"""The timeout should be configurable via PRAISONAI_SERVER_READY_TIMEOUT."""
36+
registry = _AgentServerRegistry()
37+
38+
# Create a mock FastAPI app
39+
mock_app = MagicMock()
40+
41+
# Register the app but don't set the ready event
42+
with patch.dict(registry._apps, {8001: mock_app}):
43+
with patch.dict(registry._ready_events, {8001: threading.Event()}):
44+
with patch.dict(registry._started, {8001: False}):
45+
with patch("threading.Thread.start"): # Prevent real server spawning
46+
# Test with custom timeout
47+
with patch.dict("os.environ", {"PRAISONAI_SERVER_READY_TIMEOUT": "0.05"}):
48+
start_time = time.time()
49+
result = registry.start_server_if_needed(8001, host="127.0.0.1")
50+
duration = time.time() - start_time
51+
52+
# Should respect the custom timeout and complete quickly
53+
assert result is True
54+
assert duration < 1.0 # Should be much less than default 5s
55+
56+
57+
def test_server_readiness_success_no_warning(caplog):
58+
"""A server that starts in time should not produce any warning."""
59+
registry = _AgentServerRegistry()
60+
61+
# Create a mock FastAPI app
62+
mock_app = MagicMock()
63+
ready_event = threading.Event()
64+
65+
# Register the app and signal readiness immediately
66+
with patch.dict(registry._apps, {8002: mock_app}):
67+
with patch.dict(registry._ready_events, {8002: ready_event}):
68+
with patch.dict(registry._started, {8002: False}):
69+
with patch("threading.Thread.start"): # Prevent real server spawning
70+
# Set the event immediately to simulate quick startup
71+
ready_event.set()
72+
73+
with caplog.at_level(logging.WARNING):
74+
result = registry.start_server_if_needed(8002, host="127.0.0.1")
75+
76+
# Should succeed without warnings
77+
assert result is True
78+
assert "did not become ready" not in caplog.text.lower()
79+
80+
81+
def test_default_timeout_value(monkeypatch):
82+
"""Test that the default timeout is 5.0 seconds when env var is not set."""
83+
registry = _AgentServerRegistry()
84+
85+
# Create a mock FastAPI app
86+
mock_app = MagicMock()
87+
ready_event = threading.Event()
88+
89+
# Register the app but don't set the ready event
90+
with patch.dict(registry._apps, {8003: mock_app}):
91+
with patch.dict(registry._ready_events, {8003: ready_event}):
92+
with patch.dict(registry._started, {8003: False}):
93+
with patch("threading.Thread.start"): # Prevent real server spawning
94+
# Remove only the specific env var so other env vars (PATH etc.) remain intact
95+
monkeypatch.delenv("PRAISONAI_SERVER_READY_TIMEOUT", raising=False)
96+
with patch("threading.Event.wait") as mock_wait:
97+
mock_wait.return_value = False
98+
result = registry.start_server_if_needed(8003, host="127.0.0.1")
99+
100+
# Should have called wait with default timeout of 5.0
101+
mock_wait.assert_called_once_with(timeout=5.0)
102+
assert result is True
103+
104+
105+
def test_invalid_timeout_env_var_fallback(caplog):
106+
"""Test that invalid PRAISONAI_SERVER_READY_TIMEOUT falls back to default with warning."""
107+
registry = _AgentServerRegistry()
108+
109+
# Create a mock FastAPI app
110+
mock_app = MagicMock()
111+
ready_event = threading.Event()
112+
113+
# Register the app but don't set the ready event
114+
with patch.dict(registry._apps, {8004: mock_app}):
115+
with patch.dict(registry._ready_events, {8004: ready_event}):
116+
with patch.dict(registry._started, {8004: False}):
117+
with patch("threading.Thread.start"): # Prevent real server spawning
118+
# Test with invalid timeout value
119+
with patch.dict("os.environ", {"PRAISONAI_SERVER_READY_TIMEOUT": "invalid"}):
120+
with caplog.at_level(logging.WARNING):
121+
with patch("threading.Event.wait") as mock_wait:
122+
mock_wait.return_value = False
123+
result = registry.start_server_if_needed(8004, host="127.0.0.1")
124+
125+
# Should have fallen back to 5.0 and logged warning
126+
mock_wait.assert_called_once_with(timeout=5.0)
127+
assert result is True
128+
assert "Invalid PRAISONAI_SERVER_READY_TIMEOUT" in caplog.text

0 commit comments

Comments
 (0)