fix(wiki): surface LLM root cause + fail fast on DNS/TLS/refused (RFC-012 M1 follow-up v2)

This commit is contained in:
matevip 2026-04-14 18:24:49 +08:00
parent 62ac1fb79c
commit f285db22d0

View File

@ -694,20 +694,22 @@ public class WikiProcessingService {
if (Thread.currentThread().isInterrupted()) {
throw new RuntimeException("LLM call interrupted for " + ctx, t);
}
String rootInfo = summarizeRoot(t);
if (isFatalModelError(t)) {
log.error("[Wiki] LLM unavailable (fatal) for {} after {} attempts: {}",
ctx, attempt, t.getMessage());
throw new RuntimeException("LLM unavailable: " + t.getMessage(), t);
log.error("[Wiki] LLM unavailable (fatal) for {} after {} attempts (rootCause={}): {}",
ctx, attempt, rootInfo, t.getMessage());
throw new RuntimeException("LLM unavailable (rootCause=" + rootInfo + "): " + t.getMessage(), t);
}
long elapsedMs = (System.nanoTime() - startNanos) / 1_000_000L;
if (attempt >= maxAttempts || elapsedMs >= maxTotalDurationMs) {
log.error("[Wiki] LLM exhausted for {} after {} attempts in {}ms (limits: maxAttempts={}, maxTotalDurationMs={}): {}",
ctx, attempt, elapsedMs, maxAttempts, maxTotalDurationMs, t.getMessage());
throw new RuntimeException("LLM exhausted: " + t.getMessage(), t);
log.error("[Wiki] LLM exhausted for {} after {} attempts in {}ms (limits: maxAttempts={}, maxTotalDurationMs={}, rootCause={}): {}",
ctx, attempt, elapsedMs, maxAttempts, maxTotalDurationMs, rootInfo, t.getMessage());
throw new RuntimeException("LLM exhausted after " + attempt + " attempts in " + elapsedMs
+ "ms (rootCause=" + rootInfo + "): " + t.getMessage(), t);
}
long sleepMs = Math.min(backoffMs, Math.max(0L, maxTotalDurationMs - elapsedMs));
log.warn("[Wiki] LLM transient failure for {} attempt={}/{} elapsed={}ms, retrying in {}ms: {}",
ctx, attempt, maxAttempts, elapsedMs, sleepMs, t.getMessage());
log.warn("[Wiki] LLM transient failure for {} attempt={}/{} elapsed={}ms, retrying in {}ms (rootCause={}): {}",
ctx, attempt, maxAttempts, elapsedMs, sleepMs, rootInfo, t.getMessage());
try {
Thread.sleep(sleepMs);
} catch (InterruptedException ie) {
@ -737,6 +739,19 @@ public class WikiProcessingService {
Throwable cur = t;
int depth = 0;
while (cur != null && depth < 8) {
// 按异常类型直接判 fatal DNS / 连接拒绝 / TLS 问题重试都是浪费
String className = cur.getClass().getSimpleName();
if ("UnknownHostException".equals(className)
|| "SSLHandshakeException".equals(className)
|| "CertificateException".equals(className)
|| "SSLPeerUnverifiedException".equals(className)) {
return true;
}
if ("ConnectException".equals(className) && cur.getMessage() != null
&& cur.getMessage().toLowerCase().contains("refused")) {
return true;
}
String msg = cur.getMessage();
if (msg != null) {
String m = msg.toLowerCase();
@ -768,6 +783,15 @@ public class WikiProcessingService {
|| (m.contains("safety") && m.contains("block"))) {
return true;
}
// 基础设施类永久错误关键字兜底和类名判断互补跨语言 SDK 也能抓到
if (m.contains("unknown host") || m.contains("no such host")
|| m.contains("connection refused")
|| m.contains("pkix path building failed")
|| m.contains("certificate verify failed")
|| m.contains("certificate_unknown")
|| m.contains("ssl handshake")) {
return true;
}
}
cur = cur.getCause();
depth++;
@ -775,6 +799,32 @@ public class WikiProcessingService {
return false;
}
/**
* 沿 getCause() 遍历到最深返回根因异常的 "SimpleName: message" 形式
* <p>
* Spring RestClient 会把 HTTP 层异常包装成 ResourceAccessException外层消息统一是
* "I/O error on POST request for ...: <rootMsg>"根因的异常类型HttpTimeoutException
* / UnknownHostException / SSLHandshakeException / ConnectException在最外层是看不到的
* UI 截断的时候又会把宝贵的根因关键字"rootCause=..."切掉运维排错几乎盲猜
* <p>
* 拼进最终抛出的 RuntimeException 消息里UI 就算截断也能在前几十字看见类名
*/
private String summarizeRoot(Throwable t) {
Throwable cur = t;
int depth = 0;
while (cur != null && cur.getCause() != null && cur.getCause() != cur && depth < 8) {
cur = cur.getCause();
depth++;
}
String cls = cur != null ? cur.getClass().getSimpleName() : "Unknown";
String msg = cur != null ? cur.getMessage() : null;
if (msg == null) return cls;
// 截短 message 避免把整段 HTML 错误页塞进异常链
String trimmed = msg.replaceAll("\\s+", " ").trim();
if (trimmed.length() > 200) trimmed = trimmed.substring(0, 200) + "...";
return cls + ": " + trimmed;
}
/** 瞬时错误的内部标记异常,确保空响应也能走重试路径 */
private static class TransientLlmException extends RuntimeException {
TransientLlmException(String msg) { super(msg); }