Skip to content

Per-kind progressive readiness during initial sync - #1189

Open
nadaverell wants to merge 3 commits into
mainfrom
feat/progressive-sync-readiness
Open

Per-kind progressive readiness during initial sync#1189
nadaverell wants to merge 3 commits into
mainfrom
feat/progressive-sync-readiness

Conversation

@nadaverell

@nadaverell nadaverell commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #1149.

What

On large clusters the initial informer sync takes minutes, and until now the entire UI sat behind a splash screen with every read returning 503 until the critical set — including pods, typically the slowest LIST — finished. Radar now becomes usable per kind, as each informer completes: navigate to Deployments while Pods is still listing and the full Deployments view works; the Pods view shows a loading state and appears the moment its data is complete.

Honest bound: the pods view itself can't appear faster than the pods LIST — the win is every other view, plus a legible app with visible per-kind progress instead of an opaque splash.

How it works

Readiness, not reordering. Informers already start in parallel; there is no queue to jump. The change is exposing per-kind sync state and gating reads on it:

  • k8score: OnInformersStarted hands out the cache mid-Phase-1; KindReadinessFor classifies a kind ready / pending / failed / unavailable from live informer state; GetSyncSnapshot reports per-kind progress cheaply (no lister walks — the connection endpoint polls sub-second during sync).
  • internal/k8s: a generation-guarded syncingCache handle alongside the untouched resourceCache singleton (its non-nil ⇒ synced invariant holds everywhere else). A context switch mid-construction invalidates every publish/promotion from the stale build — this also closes a pre-existing race where a construction finishing after a switch could republish the old cluster's cache.
  • server: the resource list/get handlers gate per kind — 503 {error_code: kind_sync_pending} while an informer syncs, kind_sync_failed when it never will — and serve from whichever cache exists. This also fixes a shipped false-empty bug: post-connect, promoted criticals and isEnabled-only listers (ReplicaSets, HPAs, ServiceAccounts…) could render a partial store as a complete list. Dynamic/CRD reads keep the connected gate (no dynamic cache exists mid-sync). Single-GET serves without relationship enrichment while the topology cache doesn't exist. A disconnected cluster still 503s everywhere — cache handles stand in for connectedness only during connecting.
  • web: the splash is replaced by the app shell as soon as informers start. Resource views work per kind — a pending kind stays in its loading state (an unsynced kind never renders as an empty list) and populates automatically. Views needing the full dataset (Home, Topology, Timeline, …) show a per-kind progress panel with links to ready kinds. The header carries "Loading cluster data — x of y ready". The large-list guard holds while counts are unavailable, and warm shared query caches (embedded multi-cluster mounts) are dropped before the shell can show another cluster's data.

RADAR_DEBUG_SYNC_DELAY=pods=120s (dev seam) simulates a slow kind for testing/demoing the window.

Testing

  • New deterministic tests: k8score progressive readiness via blocked-LIST fake-client reactors (pending → ready, deferred-timeout → failed), generation-guard publish/promote/clear vs reset, alias→informer-key mapping, debug-delay parsing. Full go test ./... + k8score module + tsc + frontend build green.
  • Visual-tested against a live GKE cluster with the delay seam: progress panel with per-kind links → full Deployments list (92 items) during sync → Pods loading state → automatic transition to the complete dashboard on connect. Fast-cluster path regression-checked (no delay: straight to connected, no syncStatus in payload).

Deliberate scope cuts

  • Aggregate views stay gated until connected — partial topology/audit would mislead.
  • /resource-counts isn't progressive yet, so guarded kinds (Pod/Event/ReplicaSet/EndpointSlice) hold their large-list guard until connected — the safe direction; progressive counts is a natural follow-up.
  • Events sync in the background with no deadline by design; its view polls while open rather than erroring (previously it error'd out after 3 retries mid-sync).

Note

Medium Risk
Changes cluster connection gating, cache singleton lifecycle on context switches, and what resource APIs return during sync—high user impact but guarded by generation checks and explicit readiness errors instead of silent wrong data.

Overview
Large clusters used to block the whole UI on a connecting splash until critical informers (often pods) finished listing. This PR exposes per-kind sync state so resource list/get handlers and the web shell can open one kind at a time while others are still pending.

Backend: k8score adds OnInformersStarted, KindReadinessFor (ready/pending/failed/unavailable), GetSyncSnapshot, and optional DebugSyncDelays. internal/k8s publishes a generation-guarded mid-sync syncingCache before Phase-1 completes; ResetResourceCache bumps cacheGeneration so stale constructions cannot republish an old cluster’s cache after a context switch. Resource handlers use gateResourceRead / requireConnectedOrSyncing and return 503 with kind_sync_pending or kind_sync_failed instead of serving partial stores as empty lists. Connection status includes syncStatus while connecting.

Frontend: When syncStatus is present, the app shell replaces the splash; resource views load per kind (pending kinds keep polling); Home/Topology/etc. show SyncProgressPanel until connected. React Query retries on kind_sync_pending; shared query caches are cleared when progressive sync starts to avoid cross-cluster stale data.

Reviewed by Cursor Bugbot for commit eedcbcc. Bugbot is set up for automated code reviews on this repo. Configure here.

@nadaverell
nadaverell requested a review from hisco as a code owner July 15, 2026 16:27
Comment thread web/src/context/ConnectionContext.tsx
Comment thread pkg/k8score/cache.go
Comment thread internal/k8s/cache.go
Comment thread web/src/api/client.ts
During the initial informer sync — minutes on large clusters — the whole UI
sat behind a splash screen and every read 503'd until the critical set
(including pods, typically the slowest LIST) finished. Resource views now
open per kind as each informer completes:

- k8score: OnInformersStarted hands out the cache mid-Phase-1;
  KindReadinessFor classifies kinds ready/pending/failed/unavailable from
  live informer state; GetSyncSnapshot reports per-kind progress without
  lister walks. RADAR_DEBUG_SYNC_DELAY dev seam simulates slow syncs.
- internal/k8s: generation-guarded syncingCache alongside the untouched
  resourceCache singleton — a context switch mid-construction invalidates
  every publish/promotion from the stale build instead of resurfacing the
  old cluster's cache.
- server: resource list/get handlers gate per kind (503 kind_sync_pending /
  kind_sync_failed) and serve from whichever cache exists; this also closes
  a shipped false-empty where promoted/isEnabled-only kinds could render a
  partial store as a complete list post-connect. Dynamic/CRD reads keep the
  connected gate. Connection payload carries a per-kind sync snapshot while
  connecting.
- web: progressive app shell during sync — resource views work per kind
  (pending kinds stay in loading, never an empty list), other views show a
  per-kind progress panel with links to ready kinds, header carries global
  progress. Large-list guard holds while counts are unavailable; warm shared
  query caches are dropped before the shell can render another cluster's
  data.

Closes #1149
- A manual connection retry cleared everything except the previous
  attempt's per-kind sync snapshot; drop it so the shell can't mislabel
  readiness against a fresh attempt.
- Events shares the deferred tracking map but syncs in the background with
  no deadline — the deferred-timeout flag must not classify it as
  terminally failed while its LIST is legitimately still running.
- initialSyncComplete is now set only after successful promotion, so an
  orphaned construction (context switch mid-build) can't leave the flag
  true with no cache installed.
@hisco
hisco force-pushed the feat/progressive-sync-readiness branch from e6f384a to eedcbcc Compare August 2, 2026 14:04

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit eedcbcc. Configure here.

return failureCount < 3
},
retryDelay: (failureCount: number, error: Error) =>
isKindSyncPending(error) ? 2000 : Math.min(1000 * 2 ** failureCount, 30000),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pending kinds flash empty lists

High Severity

selectedKindQueryResult maps loading from TanStack Query's isLoading, but v5 sets isLoading false during retry delays while isPending stays true. Infinite kind_sync_pending retries therefore drop the loader for ~2s gaps and the table falls through to No &lt;kind&gt; found, including long-running Events sync. That breaks the guarantee that an unsynced kind never renders as an empty list.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eedcbcc. Configure here.

Comment thread pkg/k8score/cache.go
}
}
return KindPending
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Readiness ahead of deferred listers

Medium Severity

KindReadinessFor returns KindReady from live HasSynced, but deferred listers such as ConfigMaps/Secrets still gate on isReady / deferredSynced, which lags the Phase-2 bookkeeping loop. gateResourceRead can allow a request that then hits a nil lister and a plain 503 without kind_sync_pending, so the frontend stops infinite-polling and can briefly show an empty or error state.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit eedcbcc. Configure here.

@hisco

hisco commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

@nadaverell I did a rebase of this branch, please consider if this branch should be merged

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.

Background sync

2 participants