fix: add retry tolerance to frontend polling (H3)
A single transient network error (502, timeout) no longer kills the polling loop. The frontend now tolerates up to 3 consecutive errors before abandoning. Successful fetches reset the counter. Also aligns frontend polling timeout (15 min) with backend timeout. Ref #57 (H3, M5)
This commit is contained in:
parent
f58b563a13
commit
4b1a15f49a
2 changed files with 45 additions and 6 deletions
|
|
@ -154,17 +154,49 @@ describe('useAnalysisStore', () => {
|
|||
expect(store.currentAnalysis.status).toBe('FAILED')
|
||||
})
|
||||
|
||||
it('polling stops on fetch error', async () => {
|
||||
it('polling retries on transient errors and stops after MAX_POLL_RETRIES', async () => {
|
||||
const job = { id: 'j1', status: 'PENDING', documentId: 'd1' }
|
||||
api.createAnalysis.mockResolvedValue(job)
|
||||
api.fetchAnalysis.mockRejectedValue(new Error('network'))
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
vi.spyOn(console, 'error').mockImplementation(() => {})
|
||||
|
||||
const store = useAnalysisStore()
|
||||
await store.run('d1')
|
||||
|
||||
// First two errors: still polling
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
expect(store.running).toBe(true)
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
expect(store.running).toBe(true)
|
||||
|
||||
// Third error: stops polling
|
||||
await vi.advanceTimersByTimeAsync(2000)
|
||||
expect(store.running).toBe(false)
|
||||
})
|
||||
|
||||
it('polling resets error count on successful fetch', async () => {
|
||||
const job = { id: 'j1', status: 'PENDING', documentId: 'd1' }
|
||||
api.createAnalysis.mockResolvedValue(job)
|
||||
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
|
||||
// Fail once, succeed, fail once — should NOT stop polling
|
||||
api.fetchAnalysis
|
||||
.mockRejectedValueOnce(new Error('network'))
|
||||
.mockResolvedValueOnce({ ...job, status: 'RUNNING' })
|
||||
.mockRejectedValueOnce(new Error('network'))
|
||||
.mockResolvedValueOnce({ ...job, status: 'COMPLETED' })
|
||||
|
||||
const store = useAnalysisStore()
|
||||
await store.run('d1')
|
||||
|
||||
await vi.advanceTimersByTimeAsync(2000) // error 1
|
||||
expect(store.running).toBe(true)
|
||||
await vi.advanceTimersByTimeAsync(2000) // success — resets counter
|
||||
expect(store.running).toBe(true)
|
||||
await vi.advanceTimersByTimeAsync(2000) // error 1 again
|
||||
expect(store.running).toBe(true)
|
||||
await vi.advanceTimersByTimeAsync(2000) // success — COMPLETED
|
||||
expect(store.running).toBe(false)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ export const useAnalysisStore = defineStore('analysis', () => {
|
|||
const error = ref<string | null>(null)
|
||||
const pollingInterval = ref<ReturnType<typeof setInterval> | null>(null)
|
||||
const pollingTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
|
||||
const MAX_POLLING_DURATION = 10 * 60 * 1000 // 10 minutes
|
||||
const MAX_POLLING_DURATION = 15 * 60 * 1000 // 15 minutes — aligned with backend timeout
|
||||
const MAX_POLL_RETRIES = 3
|
||||
|
||||
const currentPages = computed<Page[]>(() => {
|
||||
if (!currentAnalysis.value?.pagesJson) return []
|
||||
|
|
@ -87,9 +88,11 @@ export const useAnalysisStore = defineStore('analysis', () => {
|
|||
|
||||
function startPolling(id: string): void {
|
||||
stopPolling()
|
||||
let consecutiveErrors = 0
|
||||
pollingInterval.value = setInterval(async () => {
|
||||
try {
|
||||
const updated = await api.fetchAnalysis(id)
|
||||
consecutiveErrors = 0
|
||||
currentAnalysis.value = updated
|
||||
const idx = analyses.value.findIndex((a) => a.id === id)
|
||||
if (idx !== -1) analyses.value[idx] = updated
|
||||
|
|
@ -98,10 +101,14 @@ export const useAnalysisStore = defineStore('analysis', () => {
|
|||
running.value = false
|
||||
}
|
||||
} catch (e) {
|
||||
error.value = (e as Error).message || 'Polling error'
|
||||
console.error('Polling error', e)
|
||||
stopPolling()
|
||||
running.value = false
|
||||
consecutiveErrors++
|
||||
console.warn(`Polling error (${consecutiveErrors}/${MAX_POLL_RETRIES})`, e)
|
||||
if (consecutiveErrors >= MAX_POLL_RETRIES) {
|
||||
error.value = (e as Error).message || 'Polling error'
|
||||
console.error('Polling abandoned after retries', e)
|
||||
stopPolling()
|
||||
running.value = false
|
||||
}
|
||||
}
|
||||
}, 2000)
|
||||
pollingTimeout.value = setTimeout(() => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue