Skip to content

Commit 4082b03

Browse files
authored
fix: add health check polling to ensure backend is ready before resol… (#615)
2 parents 77069d7 + 70f3faf commit 4082b03

File tree

3 files changed

+86
-7
lines changed

3 files changed

+86
-7
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from fastapi import APIRouter
2+
from pydantic import BaseModel
3+
4+
router = APIRouter(tags=["Health"])
5+
6+
7+
class HealthResponse(BaseModel):
8+
status: str
9+
service: str
10+
11+
12+
@router.get("/health", name="health check", response_model=HealthResponse)
13+
async def health_check():
14+
"""Health check endpoint for verifying backend is ready to accept requests."""
15+
return HealthResponse(status="ok", service="eigent")
16+

backend/app/router.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
All routers are explicitly registered here for better visibility and maintainability.
44
"""
55
from fastapi import FastAPI
6-
from app.controller import chat_controller, model_controller, task_controller, tool_controller
6+
from app.controller import chat_controller, model_controller, task_controller, tool_controller, health_controller
77
from utils import traceroot_wrapper as traceroot
88

99
logger = traceroot.get_logger("router")
@@ -23,6 +23,11 @@ def register_routers(app: FastAPI, prefix: str = "") -> None:
2323
prefix: Optional global prefix for all routes (e.g., "/api")
2424
"""
2525
routers_config = [
26+
{
27+
"router": health_controller.router,
28+
"tags": ["Health"],
29+
"description": "Health check endpoint for service readiness"
30+
},
2631
{
2732
"router": chat_controller.router,
2833
"tags": ["chat"],

electron/main/init.ts

Lines changed: 64 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import log from 'electron-log'
44
import fs from 'fs'
55
import path from 'path'
66
import * as net from "net";
7+
import * as http from "http";
78
import { ipcMain, BrowserWindow, app } from 'electron'
89
import { promisify } from 'util'
910
import { detectInstallationLogs, PromiseReturnType } from "./install-deps";
@@ -195,45 +196,102 @@ export async function startBackend(setPort?: (port: number) => void): Promise<an
195196

196197

197198
let started = false;
199+
let healthCheckInterval: NodeJS.Timeout | null = null;
198200
const startTimeout = setTimeout(() => {
199201
if (!started) {
202+
if (healthCheckInterval) clearInterval(healthCheckInterval);
200203
node_process.kill();
201204
reject(new Error('Backend failed to start within timeout'));
202205
}
203206
}, 30000); // 30 second timeout
204207

208+
// Helper function to poll health endpoint
209+
const pollHealthEndpoint = (): void => {
210+
let attempts = 0;
211+
const maxAttempts = 20; // 5 seconds total (20 * 250ms)
212+
const intervalMs = 250;
213+
214+
healthCheckInterval = setInterval(() => {
215+
attempts++;
216+
const healthUrl = `http://127.0.0.1:${port}/health`;
217+
218+
const req = http.get(healthUrl, { timeout: 1000 }, (res) => {
219+
if (res.statusCode === 200) {
220+
log.info(`Backend health check passed after ${attempts} attempts`);
221+
started = true;
222+
clearTimeout(startTimeout);
223+
if (healthCheckInterval) clearInterval(healthCheckInterval);
224+
resolve(node_process);
225+
} else {
226+
// Non-200 status (e.g., 404), continue polling unless max attempts reached
227+
if (attempts >= maxAttempts) {
228+
log.error(`Backend health check failed after ${attempts} attempts with status ${res.statusCode}`);
229+
started = true;
230+
clearTimeout(startTimeout);
231+
if (healthCheckInterval) clearInterval(healthCheckInterval);
232+
node_process.kill();
233+
reject(new Error(`Backend health check failed: HTTP ${res.statusCode}`));
234+
}
235+
}
236+
});
237+
238+
req.on('error', () => {
239+
// Connection error - backend might not be ready yet, continue polling
240+
if (attempts >= maxAttempts) {
241+
log.error(`Backend health check failed after ${attempts} attempts: unable to connect`);
242+
started = true;
243+
clearTimeout(startTimeout);
244+
if (healthCheckInterval) clearInterval(healthCheckInterval);
245+
node_process.kill();
246+
reject(new Error('Backend health check failed: unable to connect'));
247+
}
248+
});
249+
250+
req.on('timeout', () => {
251+
req.destroy();
252+
if (attempts >= maxAttempts) {
253+
log.error(`Backend health check timed out after ${attempts} attempts`);
254+
started = true;
255+
clearTimeout(startTimeout);
256+
if (healthCheckInterval) clearInterval(healthCheckInterval);
257+
node_process.kill();
258+
reject(new Error('Backend health check timed out'));
259+
}
260+
});
261+
}, intervalMs);
262+
};
205263

206264
node_process.stdout.on('data', (data) => {
207265
displayFilteredLogs(data);
208266
// check output content, judge if start success
209267
if (!started && data.toString().includes("Uvicorn running on")) {
210-
started = true;
211-
clearTimeout(startTimeout);
212-
resolve(node_process);
268+
log.info('Uvicorn startup detected, starting health check polling...');
269+
pollHealthEndpoint();
213270
}
214271
});
215272

216273
node_process.stderr.on('data', (data) => {
217274
displayFilteredLogs(data);
218275

219276
if (!started && data.toString().includes("Uvicorn running on")) {
220-
started = true;
221-
clearTimeout(startTimeout);
222-
resolve(node_process);
277+
log.info('Uvicorn startup detected (stderr), starting health check polling...');
278+
pollHealthEndpoint();
223279
}
224280

225281
// Check for port binding errors
226282
if (data.toString().includes("Address already in use") ||
227283
data.toString().includes("bind() failed")) {
228284
started = true; // Prevent multiple rejections
229285
clearTimeout(startTimeout);
286+
if (healthCheckInterval) clearInterval(healthCheckInterval);
230287
node_process.kill();
231288
reject(new Error(`Port ${port} is already in use`));
232289
}
233290
});
234291

235292
node_process.on('close', (code) => {
236293
clearTimeout(startTimeout);
294+
if (healthCheckInterval) clearInterval(healthCheckInterval);
237295
if (!started) {
238296
reject(new Error(`fastapi exited with code ${code}`));
239297
}

0 commit comments

Comments
 (0)