Add UI load-test harness: synthetic fake cluster with live pod-count control - #1359
Conversation
…control testserver can now seed a topology-realistic synthetic Kubernetes population and scale the live pod count at runtime, so Radar's UI can be exercised at tens of thousands of pods with no real cluster, kubelet, or etcd. Radar runs its real informer -> topology -> SSE -> render pipeline against an in-process fake client and treats the objects as a real cluster. - internal/loadtest: generator for a coherent Deployment -> ReplicaSet -> Pod population with matching Service/ConfigMap/Secret per app, spread across fake nodes and namespaces. Deterministic names; pure function of the pod count. - cmd/testserver: -pods/-nodes/-namespaces/-pods-per-app flags seed the population at construction (delivered via the informers' initial LIST). With -pods 0 the existing nginx fixture is unchanged. A small admin listener (default <port>+1) exposes POST /loadtest/scale and GET /loadtest/status. - Live scaling mutates the fake client in batches below the fake watch channel's 100-event buffer and paces every watched kind against its informer count between batches, so it never overflows (RaceFreeFakeWatcher panics on full) and always converges. g.current advances per drained batch. - make loadtest target + docs/loadtest.md. Verified at 50k pods (store complete, ~800MB RSS) with live scale up/down and in-browser SSE count updates; unit tests cover the topology math and a scale round-trip through a live informer (-race clean). Claude-Session: https://claude.ai/code/session_01LFEw9hKzrvDUfVMf7p38SJ
…n retry A scale-down deletes pods (advancing the pod counter) before it deletes the app skeletons. If it failed in between, a retry recomputed the app count from pod progress and skipped deleting the higher-index apps, orphaning their Deployments/ReplicaSets/Services/ConfigMaps/Secrets until restart. Track the materialized app count independently and advance it per drained batch, so `appsHave` reflects the skeletons actually present rather than a pod-derived estimate. Adds a regression test that reproduces the partial-progress retry. Claude-Session: https://claude.ai/code/session_01LFEw9hKzrvDUfVMf7p38SJ
…t orphan Advancing the pod and app high-water marks only after a full batch drained meant a mid-batch Create failure (or context cancellation) left objects written to the client but unrecorded; a later scale-down then under-counted and orphaned them. Advance g.current per pod written and g.currentApps before each app's objects are written, so cleanup always covers everything that reached the client. Adds a regression test injecting a mid-scale-up Deployment create failure and asserting a subsequent scale-down leaves no skeleton of any kind in the client. Claude-Session: https://claude.ai/code/session_01LFEw9hKzrvDUfVMf7p38SJ
|
Both Cursor Bugbot findings addressed:
Each is covered by a regression test (partial-progress scale-down; injected mid-scale-up Deployment create failure asserting a subsequent scale-down leaves no skeleton of any kind in the client). |
| for j < last { | ||
| end := min(j+appBatchSize, last) | ||
| for ; j < end; j++ { | ||
| g.currentApps = j + 1 |
There was a problem hiding this comment.
Incomplete apps skipped on retry
Medium Severity
createApps bumps currentApps before the app's objects are fully created. ScaleTo then treats that index as done and starts the next create at appsHave, so a retry of the same or higher target never finishes a partially written app. Pods for that app can still be created against missing Deploy/RS/Service objects, leaving a broken synthetic topology after a cancelled or failed scale-up.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 158aede. Configure here.
… apps A partially-written app (some of its five objects created, then a failure) needs opposite handling from the two paths: cleanup must delete it, but a create retry must finish it. A single high-water mark can't serve both — recording the app before writing covered cleanup but made a retry skip it, leaving pods pointing at a missing Deployment/ReplicaSet/Service. Track two marks: appsCompleted (all five objects written — where a create resumes) and appsMaterialized (furthest app any object was written for — the cleanup delete bound). Creates are idempotent, so resuming re-completes the partial app. Adds a regression test that fails a scale-up mid-app and asserts the retry finishes it. Claude-Session: https://claude.ai/code/session_01LFEw9hKzrvDUfVMf7p38SJ
|
Third Bugbot finding addressed:
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 1909bfb. Configure here.
…pod count reconcileBoundaryApps derived the old boundary from appsFor(from), which is pod-derived. When the pod count lags the app count after a partial failure, that points the reconcile at the wrong app, so a former partial-boundary Deployment keeps a too-low replica count while its pods are full. Reconcile against the app count captured before this scale's mutations instead. Adds a regression test that diverges the pod counter below the app count and asserts the former boundary app's replicas are corrected on the next scale-up. Claude-Session: https://claude.ai/code/session_01LFEw9hKzrvDUfVMf7p38SJ
|
Fourth Bugbot finding addressed:
|


What
Lets developers drive Radar's UI at tens of thousands of pods with a live-controllable pod count, without a real cluster, real workloads, a kubelet, or etcd. Radar connects to an in-process fake Kubernetes client and runs its real informer → topology → SSE → React render pipeline, treating the synthetic objects exactly as it would a real cluster.
This is the leanest realization of the industry pattern behind KWOK ("Kubernetes WithOut Kubelet") — fake objects, no containers — but with no separate control-plane process: the objects live only as Go values in the test server's memory. The generated objects are standard Kubernetes objects, so the same generator feeds a KWOK cluster as-is if real-wire fidelity is ever wanted.
How
internal/loadtest— a generator producing a coherentDeployment → ReplicaSet → Podpopulation with a matchingService/ConfigMap/Secretper app (the pod template references the ConfigMap and Secret, so topology shows the "Configures" edges), spread across fake nodes and namespaces. Names are deterministic; the object set is a pure function of the pod count.cmd/testserver— new-pods/-nodes/-namespaces/-pods-per-appflags seed the population at construction, so it arrives through the informers' initial LIST (safe at any count). With-pods 0the existing nginx fixture is unchanged (Playwright e2e unaffected). A small admin listener (default<port>+1) exposesPOST /loadtest/scale {"pods":N}andGET /loadtest/status.RaceFreeFakeWatcherhas a 100-event buffer that panics on overflow. Mutations are therefore batched below that bound and paced against the informer's observed count between batches — for every watched kind, not just pods — advancing progress per drained batch so a mid-scale failure stays consistent.scalereturns once the Pod informer has converged.make loadtesttarget +docs/loadtest.md.Verification
Pod:50000, plus 250 each of Deployment/ReplicaSet/Service/ConfigMap/Secret), ~800 MB RSS,/api/resources/podsreturns all 50k in ~0.2s.-raceclean).A cross-model review (cursor/gpt-5.5-high) flagged that non-pod app mutations were initially unbatched (panic risk), the boundary reconcile over-patched, progress state could go stale on partial failure, and the admin bind was silent on failure — all fixed in this branch.
Known limits (documented in docs/loadtest.md)
/api/resource-countsreturns real numbers). Wiring discovery is the same step CRD kinds (HTTPRoutes, etc.) will need — the natural next extension.https://claude.ai/code/session_01LFEw9hKzrvDUfVMf7p38SJ
Note
Low Risk
Changes are confined to dev/test tooling (
testserver,loadtest, Makefile, docs) and do not alter production cluster connectivity or auth paths.Overview
Adds a UI load-testing path that runs Radar against a large, topology-realistic fake cluster (no real kubelet/etcd), with optional live pod count control over HTTP.
A new
internal/loadtestpackage seeds and mutates deterministicDeployment → ReplicaSet → Podgraphs plus per-app Services, ConfigMaps, and Secrets across fake nodes/namespaces.ScaleTobatches creates/deletes and waits on informer counts so the fake client-go watch buffer (100 events) does not panic; partial-failure handling avoids orphan app skeletons.cmd/testservergains-pods/-nodes/-namespaces/-pods-per-app(load-test mode) while-pods 0keeps the existing nginx Playwright fixture. When load-test mode is on, an admin listener on<port>+1exposesGET /loadtest/statusandPOST /loadtest/scale, pacing against Radar’s resource cache viainformerCount.make loadtest(default 50k pods) anddocs/loadtest.mddocument usage, live scaling, and known limits (e.g. no API discovery for grouped-kind sidebar badges). Unit tests cover topology math, informer round-trips, and failure/retry/orphan cleanup scenarios.Reviewed by Cursor Bugbot for commit 67abbc9. Bugbot is set up for automated code reviews on this repo. Configure here.