perf(feishu): prefetch bot open_id + DCL + 60s negative cache

This commit is contained in:
matevip 2026-05-20 10:13:40 +08:00
parent 73ab31a13c
commit 3c3c2a938f

View File

@ -99,9 +99,23 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
/** 旧事件过滤默认阈值(秒):超过 30 秒的事件视为重连后回放 */ /** 旧事件过滤默认阈值(秒):超过 30 秒的事件视为重连后回放 */
private static final long DEFAULT_STALE_THRESHOLD_SECONDS = 30L; private static final long DEFAULT_STALE_THRESHOLD_SECONDS = 30L;
/** 机器人自身 open_id 缓存(用于 require_mention 精确判断,懒加载) */ /** Bot's own open_id, fetched once from /open-apis/bot/v3/info and cached. */
private volatile String botOpenId; private volatile String botOpenId;
/** Serializes lazy bot-open-id fetches so concurrent group messages share one API roundtrip. */
private final Object botOpenIdLock = new Object();
/**
* Last failure timestamp for {@code /open-apis/bot/v3/info}. While the call is in the
* {@link #BOT_OPENID_FAILURE_BACKOFF_MS} negative-cache window, {@link #getBotOpenId()}
* returns {@code null} fast so a Feishu outage doesn't trigger one synchronous retry
* per inbound group message.
*/
private volatile long botOpenIdLastFailureMs = 0L;
/** Negative-cache window for {@link #getBotOpenId()} failures (60 s). */
private static final long BOT_OPENID_FAILURE_BACKOFF_MS = 60_000L;
public FeishuChannelAdapter(ChannelEntity channelEntity, public FeishuChannelAdapter(ChannelEntity channelEntity,
ChannelMessageRouter messageRouter, ChannelMessageRouter messageRouter,
ObjectMapper objectMapper) { ObjectMapper objectMapper) {
@ -132,6 +146,12 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
// 定时刷新 Token过期前 5 分钟自动刷新 // 定时刷新 Token过期前 5 分钟自动刷新
scheduleTokenRefresh(); scheduleTokenRefresh();
// Prefetch the bot's own open_id once the token is valid so the first
// require_mention check on the WebSocket dispatch thread doesn't pay
// the 5 s API latency. Failure is non-fatal getBotOpenId() handles
// it and the negative cache keeps subsequent retries cheap.
getBotOpenId();
String connectionMode = getConfigString("connection_mode", "websocket"); String connectionMode = getConfigString("connection_mode", "websocket");
if ("websocket".equals(connectionMode)) { if ("websocket".equals(connectionMode)) {
startWebSocket(appId, appSecret); startWebSocket(appId, appSecret);
@ -166,6 +186,8 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
this.httpClient = null; this.httpClient = null;
this.tenantAccessToken = null; this.tenantAccessToken = null;
this.botOpenId = null;
this.botOpenIdLastFailureMs = 0L;
this.processedMessageIds.clear(); this.processedMessageIds.clear();
this.nicknameCache.clear(); this.nicknameCache.clear();
this.quotedMessageCache.clear(); this.quotedMessageCache.clear();
@ -189,6 +211,10 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
log.warn("[feishu] Token refresh during reconnect failed: {}", e.getMessage()); log.warn("[feishu] Token refresh during reconnect failed: {}", e.getMessage());
} }
// Re-prefetch bot open_id after reconnect: same dispatch-thread latency
// concern as doStart, plus picks up a rotated app identity if any.
getBotOpenId();
if ("websocket".equals(connectionMode)) { if ("websocket".equals(connectionMode)) {
log.info("[feishu] Reconnecting WebSocket..."); log.info("[feishu] Reconnecting WebSocket...");
stopWebSocket(); stopWebSocket();
@ -459,33 +485,59 @@ public class FeishuChannelAdapter extends AbstractChannelAdapter {
} }
/** /**
* Fetches the bot's own open_id once and caches it. Returns {@code null} when * Fetches the bot's own open_id once and caches it. Returns {@code null}
* the fetch fails callers that consult this value (currently the * when the identity is unavailable; callers (currently the
* {@code require_mention} gate) must treat {@code null} as "identity unknown" * {@code require_mention} gate) treat {@code null} as "identity unknown"
* and fall open so a transient API outage doesn't silence the bot. * and fall open so a transient API outage doesn't silence the bot.
*
* <p>Concurrent callers share a single API roundtrip via
* {@link #botOpenIdLock}. On failure, {@link #botOpenIdLastFailureMs} is
* stamped so callers within the next {@link #BOT_OPENID_FAILURE_BACKOFF_MS}
* ms return {@code null} immediately instead of triggering a fresh 5 s
* synchronous fetch per inbound message.
*/ */
private String getBotOpenId() { private String getBotOpenId() {
if (botOpenId != null) return botOpenId; String cached = botOpenId;
try { if (cached != null) return cached;
ensureTokenValid(); if (withinFailureBackoff()) return null;
String apiBase = getApiBaseUrl(); synchronized (botOpenIdLock) {
HttpRequest request = HttpRequest.newBuilder() // Re-check under the lock another thread may have populated the
.uri(URI.create(apiBase + "/open-apis/bot/v3/info")) // cache or stamped a fresh failure while we were waiting.
.header("Authorization", "Bearer " + tenantAccessToken) if (botOpenId != null) return botOpenId;
.GET() if (withinFailureBackoff()) return null;
.timeout(Duration.ofSeconds(5)) try {
.build(); ensureTokenValid();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); String apiBase = getApiBaseUrl();
Map<?, ?> body = objectMapper.readValue(response.body(), Map.class); HttpRequest request = HttpRequest.newBuilder()
Map<?, ?> bot = (Map<?, ?>) body.get("bot"); .uri(URI.create(apiBase + "/open-apis/bot/v3/info"))
if (bot != null) { .header("Authorization", "Bearer " + tenantAccessToken)
botOpenId = (String) bot.get("open_id"); .GET()
log.info("[feishu] Bot open_id fetched and cached: {}", botOpenId); .timeout(Duration.ofSeconds(5))
.build();
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
Map<?, ?> body = objectMapper.readValue(response.body(), Map.class);
Map<?, ?> bot = (Map<?, ?>) body.get("bot");
if (bot != null && bot.get("open_id") instanceof String openId && !openId.isBlank()) {
botOpenId = openId;
log.info("[feishu] Bot open_id fetched and cached: {}", openId);
return openId;
}
// 2xx with no bot.open_id field treat as transient failure.
botOpenIdLastFailureMs = System.currentTimeMillis();
log.warn("[feishu] /open-apis/bot/v3/info returned no bot.open_id; require_mention gate falls open for {}s",
BOT_OPENID_FAILURE_BACKOFF_MS / 1000);
} catch (Exception e) {
botOpenIdLastFailureMs = System.currentTimeMillis();
log.warn("[feishu] Failed to fetch bot open_id (require_mention gate falls open for {}s): {}",
BOT_OPENID_FAILURE_BACKOFF_MS / 1000, e.getMessage());
} }
} catch (Exception e) { return null;
log.warn("[feishu] Failed to fetch bot open_id; require_mention gate will fall open until next attempt: {}", e.getMessage());
} }
return botOpenId; }
private boolean withinFailureBackoff() {
long last = botOpenIdLastFailureMs;
return last != 0L && System.currentTimeMillis() - last < BOT_OPENID_FAILURE_BACKOFF_MS;
} }
// ==================== Token 管理 ==================== // ==================== Token 管理 ====================