Skip to content

fix(#1138): fix SSE EventSource leak and polling race condition (duplicate, conflicts resolved)#1234

Open
ionfwsrijan wants to merge 2 commits into
utksh1:mainfrom
ionfwsrijan:duplicate/pr-1174-sse-polling-race
Open

fix(#1138): fix SSE EventSource leak and polling race condition (duplicate, conflicts resolved)#1234
ionfwsrijan wants to merge 2 commits into
utksh1:mainfrom
ionfwsrijan:duplicate/pr-1174-sse-polling-race

Conversation

@ionfwsrijan

@ionfwsrijan ionfwsrijan commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Duplicate of #1174 with merge conflicts resolved and rebased onto latest main.

Problem

  1. SSE EventSource leak: On reconnect, the es.onerror handler could fire
    for the old EventSource after a new connection was established, triggering
    spurious reconnection loops and duplicate EventSources.
  2. Polling race: setInterval(loadTasks, 5000) in Scans.tsx doesn't await
    the async loadTasks(). If a request takes longer than 5s, overlapping
    requests pile up, potentially corrupting state.

Solution

  • Added a versionRef (generation counter) to useTaskSubscription.ts that
    increments on every connectSSE() call and every cleanupAll()
  • All SSE callbacks (status, phase, output, onerror, onopen) now check
    versionRef.current === expectedVersion before acting — stale callbacks from
    previous connections are silently ignored
  • In Scans.tsx, replaced setInterval with chained setTimeout:
    scheduleNextPoll() awaits loadTasks() before scheduling the next tick
  • Renamed intervalRefpollingTimerRef for clarity

Files changed

frontend/src/hooks/useTaskSubscription.ts, frontend/src/pages/Scans.tsx

Closes #1138

- Add version/generation counter in useTaskSubscription to prevent stale
  EventSource callbacks from triggering after reconnection
- Check versionRef.current against expected version in all SSE event handlers,
  onerror, and onopen to ignore callbacks from previous connections
- Replace setInterval in Scans.tsx with chained setTimeout that awaits
  loadTasks() before scheduling the next poll
- Use AbortController signal to gate polling continuation
- Rename intervalRef to pollingTimerRef for clarity
…eout

The chained setTimeout pattern calls poll() once immediately on start,
unlike setInterval which waits for the first interval. Update the test
assertions to reflect the correct call counts.
@ionfwsrijan

Copy link
Copy Markdown
Contributor Author

@utksh1 Please review this now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] SSE Connection Leak in useTaskSubscription Combined with Polling Race in Scans Page

1 participant