fix(config): poll for reloaded org instead of reload counter in flaky watcher test - #5464
Conversation
… watcher test TestWatcher_ReloadsOnChange failed the required v2 Tests gate on PRs that do not touch pkg/config (e.g. #5440, run 33488167240, job `test (rest 2/3)`) with: watcher_test.go:62: expected org = "updated-org" after reload, got "" Root cause is a race in the test, not in the watcher. The onChange callback increments reloadCount before it stores lastOrg: reloadCount.Add(1) lastOrg.Store(cfg.Project.Org) while the wait loop exited as soon as reloadCount > 0 and then immediately read lastOrg. When the poll observes the counter in the window between those two statements, lastOrg is still unset and the type assertion yields "" — the exact reported failure. The window is tiny locally but is readily hit under -race on a loaded CI runner. Note this is NOT a partial/truncated read of a non-atomic os.WriteFile as first suspected: every truncation of the config fails to parse or fails validation ("project.org is required"), so onChange is never invoked and no empty org can ever be stored. The observed "" is the never-stored case. Fix: poll for the observable outcome (lastOrg == "updated-org") rather than for the reload counter, with a generous deadline and a tighter poll interval. The assertion is unchanged in strength — the test still proves the watcher reloads and delivers the new content, and still requires reloadCount > 0. Verification, all under -short -race: - Reproduced deterministically by injecting a stall between the increment and the store (modelling a scheduler preemption): 10/10 failures with the identical message, 0/25 after the fix; still 0/10 with a 3s stall. - Real test: 200/200 pass; all three TestWatcher_* 100x under 12-way CPU saturation: pass. Fixes #5448 Signed-off-by: Andrew Anderson <andy@clubanderson.com>
|
[APPROVALNOTIFIER] This PR is APPROVED Approval requirements bypassed by manually added approval. This pull-request has been approved by: The full list of commands accepted by this bot can be found here. The pull request process is described here DetailsNeeds approval from an approver in each of these files:Approvers can indicate their approval by writing |
|
Changelog: this PR changes code but does not touch If it is user-visible — a feature, a fix an operator would notice, a This is a reminder, not a gate; it never blocks a merge. |
|
Thank you for your contribution! Your PR has been merged. Check out what's new:
Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey |
Fixes #5448
Root cause — a race in the test, not in the watcher
TestWatcher_ReloadsOnChangefailed the required v2 Tests gate on PRs that do not touchpkg/config(#5440, run 33488167240, jobtest (rest 2/3)):The
onChangecallback increments the counter before it stores the value:while the wait loop exited as soon as
reloadCount > 0and then immediately readlastOrg. If the poll observes the counter in the window between those two statements,lastOrgis still unset, the type assertionlastOrg.Load().(string)returnsok=falsewith the zero value, and the test reportsgot "".The 50ms poll interval usually hides the window; a scheduler stall under
-raceon a loaded runner lands the observation inside it.This is not the originally suspected cause
The issue proposed a partial read of a non-atomic
os.WriteFile. That mechanism cannot produce this failure — every truncation of the config is rejected beforeonChangeruns:LoadWithDashboardOverlayresultproject.org is requiredyaml: unmarshal errorscould not find expected ':'project.org is requiredA failed load returns early and never invokes
onChange, so no empty org can ever be stored. The observed""is the never-stored case, which is why the fix targets the observation order rather than write atomicity.Fix
Poll for the observable outcome (
lastOrg == "updated-org") instead of the reload counter, with a generous deadline and a tighter poll interval.The assertion is not weakened: the test still requires
reloadCount > 0and still requires the reloaded config to carry exactlyupdated-org. A watcher that failed to reload, or reloaded stale content, still fails — now by timing out rather than by reading a half-published result. The failure message also reports the reload count for easier diagnosis.Verification (all
-short -race)Reproduced deterministically by injecting a stall between the increment and the store, modelling a scheduler preemption:
-count=200TestWatcher_*,-count=100under 12-way CPU saturationThe pre-fix run reproduces the exact CI message and line, and the fix survives a 3s stall — far beyond any plausible CI delay, so this is not merely a retuned timeout.
Notes
watcher.gois untouched. The watcher itself is correct — no product race was found.TestEntrypointHardensRuntimeConfigfails locally on macOS both with and without this change (pre-existingchown/file-mode limitation, 🐛 arm64 startup lane broken on v4: hive.yaml.runtime permission denied after #5352 #5360/[sec-check] dashboard owner auth token persisted world-readable in /data/hive.yaml.runtime, .dashboard, and .bak (0644) #5331); unrelated and out of scope. The rest ofpkg/configis green.🤖 Generated with Claude Code