Skip to content

fix(dashboard): keep extension progress polls single-flight - #3065

Open
tang-vu wants to merge 2 commits into
Osmantic:mainfrom
tang-vu:fix/extension-progress-single-flight
Open

fix(dashboard): keep extension progress polls single-flight#3065
tang-vu wants to merge 2 commits into
Osmantic:mainfrom
tang-vu:fix/extension-progress-single-flight

Conversation

@tang-vu

@tang-vu tang-vu commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Why this matters

The Extensions page polls installation progress every 3 seconds, while each request is allowed to run for up to 8 seconds. Its logs modal has a second 5-second progress interval with the same timeout. On a slow or restarting dashboard-api, either loop could therefore issue multiple progress requests before its previous request settled. Those requests independently update recovery counts, refresh catalog/terminal state, or update modal state, so one slow response can cause request amplification, premature connection-lost state, duplicate terminal handling, and stale UI writes.

These paths are reachable whenever a user installs/enables an extension, reloads during installing/setting_up, or opens an extension's Logs modal.

Root cause and invariant

Root cause: activePollers prevented duplicate interval creation per service but did not prevent later async ticks from overlapping. The modal interval had no in-flight guard either.

Invariant: each progress polling loop has at most one request in flight; different extensions remain independently pollable, and each loop resumes on the first scheduled tick after its pending request settles.

What changed

  • Track page-level progress requests in flight per service ID.
  • Add an effect-local in-flight guard to the Logs modal progress poll.
  • Skip only ticks whose own prior request remains pending.
  • Release both guards on every settlement path, including success, HTTP/JSON failures, and aborts.
  • Clear the page-level guard map with existing interval/tracker cleanup on unmount.
  • Add fake-timer component regressions for both loops with deferred fetches: elapsed intervals do not overlap, then polling resumes after settlement.

Overlap check

Searched open and closed PRs for extension install/progress polling, activePollers, overlapping polls, and scanned all open PRs changing Extensions.jsx.

Focused validation

Red/green reproductions:

  • Before each production guard, its focused deferred-fetch test failed by observing 2 progress calls while the first promise remained unresolved.
  • After both guards: npx vitest run src/pages/Extensions.test.jsx -t "progress polling single-flight" ? 2 passed.

Affected dashboard gate:

  • npm test -- --run ? 28 files, 260 tests passed
  • npm run lint ? passed
  • npm run build ? passed (only the existing stale Browserslist data notice)
  • git diff --check ? passed (Git emitted only Windows LF/CRLF checkout notices)

The tests prove browser scheduling and fetch concurrency at the rendered page and modal boundaries. They do not simulate a live extension image pull or dashboard-api restart.

Design, compatibility, and rollback

The per-service page guard preserves concurrent installs of different extensions; the modal-local guard does not couple modal work to the parent loop. Existing cadences, timeout, recovery tracker, and terminal state machine stay intact. Missed ticks are skipped rather than queued so a slow backend cannot accumulate work; the next normal tick retries after settlement.

Rollback is a normal revert of the two commits and restores overlapping requests whenever response latency exceeds an interval.

@tang-vu

tang-vu commented Aug 23, 2026

Copy link
Copy Markdown
Contributor Author

Batch compatibility receipt (2026-08-24)

Built the inspectable synthetic branch tang-vu:validation/batch-20260824-architecture from upstream main (6ff9b4fc) and merged exact candidate heads in this order:

  1. fix(dashboard): keep extension progress polls single-flight #3065 bad25e7f
  2. feat(extensions): preview and confirm lifecycle change plans #2903 829dd48f
  3. feat(observability): stream scoped service health transitions #2911 d8aed4c6
  4. feat(extensions): inspect and gate dependency graphs #2909 6e9bd227
  5. feat(release): publish commit-bound deterministic runtime SBOM #2894 9115d147
  6. refactor(cli): extract read-only formatting primitives #3071 bff5cbfc

No manual merge resolution was required, including the shared Extensions.jsx / Extensions.test.jsx surface in #3065 and #2903. Synthetic head: 2c82aaec.

Combined validation:

  • Dashboard: 28 files / 262 tests passed; ESLint passed; production Vite build passed
  • Dashboard API: 350 passed, 5 skipped across service events, main routing, extension plans, lifecycle, and dependency suites
  • dependency graph + manifest gates: 4 pytest cases, 8 shell cases, full canonical/library schema contract passed
  • runtime SBOM: 5 tests passed; shipped dependency-pin validation passed
  • CLI: JSON public boundary, Phase C smoke, Bash syntax, ShellCheck, 4/4 BATS formatting contract, and enable/reconciliation fixture passed

The stale Browserslist notice and authentication-dependent Phase C skips are unchanged environmental disclosures. #2903 remains draft because synthetic compatibility does not replace its declared live host-agent lifecycle gate and independent human review.

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.

1 participant