SIGN IN SIGN UP

[Store] Arm the etcd view-change watch once per wait instead of per iteration (#3062)

WaitForViewChange armed a fresh watch (new ViewChangeWatchState + watch
goroutine) on every loop iteration. Each iteration's PrefixWatchGuard
destructor then had kWatchStopTimeoutMs (5s) for the goroutine to exit;
whenever it missed the budget the guard deliberately leaked the state to
avoid UAF, and the goroutine with its OS thread accumulated — visible in
HA mode as a steady thread climb (~7/min per client process under load).

One persistent watch now serves every wait inside the call: spurious
events just clear the flag and the same goroutine keeps delivering, and
the only re-arm path is etcd reporting the watch broken (event_type 2,
now surfaced through a new `broken` flag on the watch state). The single
cancel+stop cycle at function exit (PrefixWatchGuard's existing bounded
cleanup) is the only one left, so the per-iteration leak is gone by
construction. The arm-before-read revision invariant and the
arm-failure fallback poll are unchanged.

Fixes #3059

Signed-off-by: Yufeng He <40085740+he-yufeng@users.noreply.github.com>
Y
Yufeng He committed
a9da850ea60bc672c41d1cc7d1024d4c2702e23b
Parent: a6dcdcd
Committed by GitHub <noreply@github.com> on 7/24/2026, 3:33:38 AM