fix(connection): use lightweight heartbeat with single-flight and fail threshold
This commit is contained in:
parent
971f29b059
commit
4e7da0de23
@ -69,6 +69,28 @@ def _is_host_mode_request() -> bool:
|
||||
return bool(session.get("host_mode")) and (TERMINAL_SANDBOX_MODE or "").lower() == "host"
|
||||
|
||||
|
||||
@status_bp.route('/api/health')
|
||||
@api_login_required
|
||||
def get_health():
|
||||
"""轻量健康检查:用于前端连接心跳,避免触发重型 status 计算。"""
|
||||
started_at = time.perf_counter()
|
||||
heartbeat_id = request.headers.get("X-Connection-Heartbeat", "").strip()
|
||||
username = session.get("username") or "-"
|
||||
payload = {
|
||||
"success": True,
|
||||
"status": "ok",
|
||||
"server_time_ms": int(time.time() * 1000),
|
||||
}
|
||||
elapsed_ms = (time.perf_counter() - started_at) * 1000
|
||||
if request.args.get("diag") == "1" or elapsed_ms >= 800:
|
||||
log_conn_diag(
|
||||
f"health hb={heartbeat_id or '-'} user={username} "
|
||||
f"elapsed_ms={elapsed_ms:.1f} host_mode={bool(session.get('host_mode'))} "
|
||||
f"workspace_id={session.get('workspace_id') or '-'}"
|
||||
)
|
||||
return jsonify(payload)
|
||||
|
||||
|
||||
@status_bp.route('/api/status')
|
||||
@api_login_required
|
||||
@with_terminal
|
||||
|
||||
@ -2025,18 +2025,32 @@ export const uiMethods = {
|
||||
},
|
||||
|
||||
async checkConnectionHealth() {
|
||||
if (this.connectionHeartbeatInFlight) {
|
||||
connectionDiag('log', 'health-skip-inflight', {
|
||||
seq: this.connectionHeartbeatSeq,
|
||||
isConnected: !!this.isConnected,
|
||||
failCount: this.connectionHeartbeatFailCount
|
||||
});
|
||||
return;
|
||||
}
|
||||
this.connectionHeartbeatInFlight = true;
|
||||
const seq = Number(this.connectionHeartbeatSeq || 0) + 1;
|
||||
this.connectionHeartbeatSeq = seq;
|
||||
const requestId = `${Date.now()}-${seq}`;
|
||||
const startedAt = Date.now();
|
||||
const diagEnabled = isConnectionDiagEnabled();
|
||||
const statusUrl = diagEnabled ? '/api/status?diag=1' : '/api/status';
|
||||
const healthUrl = diagEnabled ? '/api/health?diag=1' : '/api/health';
|
||||
const wasConnected = !!this.isConnected;
|
||||
let responseStatus: number | null = null;
|
||||
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
||||
const timeoutId = controller ? window.setTimeout(() => controller.abort(), 5000) : null;
|
||||
const timeoutMs =
|
||||
typeof this.connectionHeartbeatRequestTimeoutMs === 'number' &&
|
||||
this.connectionHeartbeatRequestTimeoutMs > 0
|
||||
? this.connectionHeartbeatRequestTimeoutMs
|
||||
: 5000;
|
||||
const timeoutId = controller ? window.setTimeout(() => controller.abort(), timeoutMs) : null;
|
||||
try {
|
||||
const response = await fetch(statusUrl, {
|
||||
const response = await fetch(healthUrl, {
|
||||
method: 'GET',
|
||||
cache: 'no-store',
|
||||
headers: {
|
||||
@ -2066,6 +2080,7 @@ export const uiMethods = {
|
||||
failCountBeforeRecover,
|
||||
status: responseStatus,
|
||||
diagEnabled,
|
||||
endpoint: healthUrl,
|
||||
conversationId: this.currentConversationId || null,
|
||||
taskInProgress: !!this.taskInProgress,
|
||||
streamingMessage: !!this.streamingMessage,
|
||||
@ -2083,6 +2098,7 @@ export const uiMethods = {
|
||||
seq,
|
||||
elapsedMs: this.connectionHeartbeatLastLatencyMs,
|
||||
status: responseStatus,
|
||||
endpoint: healthUrl,
|
||||
visibility: document?.visibilityState || 'unknown'
|
||||
});
|
||||
}
|
||||
@ -2094,12 +2110,20 @@ export const uiMethods = {
|
||||
const errName = error?.name || 'Error';
|
||||
const errMessage = error?.message || String(error);
|
||||
this.connectionHeartbeatLastError = `${errName}: ${errMessage}`;
|
||||
// 一次失败就先置灰,避免“服务已断但常亮绿色”的误导
|
||||
this.isConnected = false;
|
||||
if (wasConnected) {
|
||||
const failThreshold =
|
||||
typeof this.connectionHeartbeatFailThreshold === 'number' &&
|
||||
this.connectionHeartbeatFailThreshold > 0
|
||||
? this.connectionHeartbeatFailThreshold
|
||||
: 3;
|
||||
const shouldDisconnect = nextFailCount >= failThreshold;
|
||||
// 改为连续失败阈值后再置灰,避免偶发请求超时导致“假断连”
|
||||
if (shouldDisconnect) {
|
||||
this.isConnected = false;
|
||||
}
|
||||
if (wasConnected && shouldDisconnect) {
|
||||
this.connectionHeartbeatLastChangeAt = Date.now();
|
||||
}
|
||||
const shouldLogFail = wasConnected || nextFailCount <= 3 || nextFailCount % 10 === 0;
|
||||
const shouldLogFail = wasConnected || nextFailCount <= failThreshold || nextFailCount % 10 === 0;
|
||||
connectionDiag(
|
||||
shouldLogFail ? 'warn' : 'log',
|
||||
'health-failed',
|
||||
@ -2109,9 +2133,12 @@ export const uiMethods = {
|
||||
elapsedMs: this.connectionHeartbeatLastLatencyMs,
|
||||
failCount: nextFailCount,
|
||||
status: responseStatus,
|
||||
endpoint: healthUrl,
|
||||
diagEnabled,
|
||||
wasConnected,
|
||||
nowConnected: !!this.isConnected,
|
||||
failThreshold,
|
||||
shouldDisconnect,
|
||||
errorName: errName,
|
||||
errorMessage: errMessage,
|
||||
timeout: errName === 'AbortError',
|
||||
@ -2125,6 +2152,7 @@ export const uiMethods = {
|
||||
{ force: shouldLogFail }
|
||||
);
|
||||
} finally {
|
||||
this.connectionHeartbeatInFlight = false;
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
@ -144,6 +144,9 @@ export function dataState() {
|
||||
connectionHeartbeatLastError: '',
|
||||
connectionHeartbeatLastStatusCode: null,
|
||||
connectionHeartbeatLastChangeAt: 0,
|
||||
connectionHeartbeatInFlight: false,
|
||||
connectionHeartbeatFailThreshold: 3,
|
||||
connectionHeartbeatRequestTimeoutMs: 5000,
|
||||
connectionHeartbeatIntervalMs: 8000,
|
||||
connectionHeartbeatDisconnectedIntervalMs: 1000,
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user