fix(export): prevent card stats poll overlap with frontend/backend singleflight

This commit is contained in:
tisonhuang
2026-03-03 10:20:58 +08:00
parent 84b54e43aa
commit b6a2191e38
2 changed files with 319 additions and 238 deletions

View File

@@ -276,6 +276,12 @@ class ChatService {
private exportContentStatsRefreshPromise: Promise<void> | null = null
private exportContentStatsRefreshQueued = false
private exportContentStatsRefreshForceQueued = false
private exportContentSessionCountsInFlight: {
promise: Promise<{ success: boolean; data?: ExportContentSessionCounts; error?: string }>
forceRefresh: boolean
traceId: string
startedAt: number
} | null = null
private exportContentStatsDirtySessionIds = new Set<string>()
private exportContentScopeSessionIdsCache: { ids: string[]; updatedAt: number } | null = null
private readonly exportContentScopeSessionIdsCacheTtlMs = 60 * 1000
@@ -2124,30 +2130,50 @@ class ChatService {
traceId?: string
}): Promise<{ success: boolean; data?: ExportContentSessionCounts; error?: string }> {
const traceId = this.normalizeExportDiagTraceId(options?.traceId)
const forceRefresh = options?.forceRefresh === true
const triggerRefresh = options?.triggerRefresh !== false
const stepStartedAt = this.startExportDiagStep({
traceId,
stepId: 'backend-get-export-content-session-counts',
stepName: '获取导出卡片统计',
message: '开始计算导出卡片统计',
data: {
triggerRefresh: options?.triggerRefresh !== false,
forceRefresh: options?.forceRefresh === true
triggerRefresh,
forceRefresh
}
})
let stepSuccess = false
let stepError = ''
let stepResult: ExportContentSessionCounts | undefined
let activePromise: Promise<{ success: boolean; data?: ExportContentSessionCounts; error?: string }> | null = null
let createdInFlight = false
try {
if (this.exportContentSessionCountsInFlight) {
this.logExportDiag({
traceId,
source: 'backend',
level: 'info',
message: '复用进行中的导出卡片统计任务',
stepId: 'backend-get-export-content-session-counts',
stepName: '获取导出卡片统计',
status: 'running',
data: {
inFlightTraceId: this.exportContentSessionCountsInFlight.traceId,
inFlightForceRefresh: this.exportContentSessionCountsInFlight.forceRefresh,
requestedForceRefresh: forceRefresh,
inFlightElapsedMs: Date.now() - this.exportContentSessionCountsInFlight.startedAt
}
})
activePromise = this.exportContentSessionCountsInFlight.promise
} else {
const createdPromise = (async () => {
const connectResult = await this.ensureConnected()
if (!connectResult.success) {
stepError = connectResult.error || '数据库未连接'
return { success: false, error: connectResult.error || '数据库未连接' }
}
this.refreshSessionMessageCountCacheScope()
const forceRefresh = options?.forceRefresh === true
const triggerRefresh = options?.triggerRefresh !== false
const sessionIds = await this.listExportContentScopeSessionIds(forceRefresh, traceId)
const sessionIdSet = new Set(sessionIds)
@@ -2180,10 +2206,10 @@ class ChatService {
const hasMediaSnapshot = Boolean(entry && entry.mediaReady)
if (hasMediaSnapshot) {
if (entry!.hasVoice) voiceSessions += 1
if (entry!.hasImage) imageSessions += 1
if (entry!.hasVideo) videoSessions += 1
if (entry!.hasEmoji) emojiSessions += 1
if (entry.hasVoice) voiceSessions += 1
if (entry.hasImage) imageSessions += 1
if (entry.hasVideo) videoSessions += 1
if (entry.hasEmoji) emojiSessions += 1
} else {
pendingMediaSessionSet.add(sessionId)
}
@@ -2267,7 +2293,9 @@ class ChatService {
void this.startExportContentStatsRefresh(false, traceId)
}
stepResult = {
return {
success: true,
data: {
totalSessions: sessionIds.length,
textSessions,
voiceSessions,
@@ -2278,16 +2306,38 @@ class ChatService {
updatedAt: this.exportContentStatsScopeUpdatedAt,
refreshing: this.exportContentStatsRefreshPromise !== null
}
stepSuccess = true
return {
success: true,
data: stepResult
}
})()
activePromise = createdPromise
this.exportContentSessionCountsInFlight = {
promise: createdPromise,
forceRefresh,
traceId,
startedAt: Date.now()
}
createdInFlight = true
}
if (!activePromise) {
stepError = '统计任务未初始化'
return { success: false, error: stepError }
}
const result = await activePromise
stepSuccess = result.success
if (result.success && result.data) {
stepResult = result.data
} else {
stepError = result.error || '未知错误'
}
return result
} catch (e) {
console.error('ChatService: 获取导出内容会话统计失败:', e)
stepError = String(e)
return { success: false, error: String(e) }
} finally {
if (createdInFlight && activePromise && this.exportContentSessionCountsInFlight?.promise === activePromise) {
this.exportContentSessionCountsInFlight = null
}
this.endExportDiagStep({
traceId,
stepId: 'backend-get-export-content-session-counts',

View File

@@ -971,6 +971,8 @@ function ExportPage() {
const activeTaskCountRef = useRef(0)
const hasBaseConfigReadyRef = useRef(false)
const contentSessionCountsForceRetryRef = useRef(0)
const contentSessionCountsInFlightRef = useRef<Promise<void> | null>(null)
const contentSessionCountsInFlightTraceRef = useRef('')
const appendFrontendDiagLog = useCallback((entry: ExportCardDiagLogEntry) => {
setFrontendDiagLogs(prev => {
@@ -1537,8 +1539,25 @@ function ExportPage() {
}, [])
const loadContentSessionCounts = useCallback(async (options?: { silent?: boolean; forceRefresh?: boolean }) => {
if (contentSessionCountsInFlightRef.current) {
logFrontendDiag({
level: 'info',
stepId: 'frontend-load-content-session-counts',
stepName: '前端请求导出卡片统计',
status: 'running',
message: '统计请求仍在进行中,跳过本次轮询',
data: {
silent: options?.silent === true,
forceRefresh: options?.forceRefresh === true,
inFlightTraceId: contentSessionCountsInFlightTraceRef.current || undefined
}
})
return
}
const traceId = createExportDiagTraceId()
const startedAt = Date.now()
const task = (async () => {
logFrontendDiag({
traceId,
stepId: 'frontend-load-content-session-counts',
@@ -1567,7 +1586,7 @@ function ExportPage() {
stepName: '前端请求导出卡片统计',
status: 'timeout',
durationMs: Date.now() - startedAt,
message: '导出卡片统计请求超时3200ms'
message: '导出卡片统计请求超时3200ms,后台可能仍在处理'
})
return
}
@@ -1662,6 +1681,18 @@ function ExportPage() {
data: { error: String(error) }
})
}
})()
contentSessionCountsInFlightRef.current = task
contentSessionCountsInFlightTraceRef.current = traceId
try {
await task
} finally {
if (contentSessionCountsInFlightRef.current === task) {
contentSessionCountsInFlightRef.current = null
contentSessionCountsInFlightTraceRef.current = ''
}
}
}, [logFrontendDiag])
const loadSessions = useCallback(async () => {