fix(main): stop the 1s reclaim ticker from racing the coordinator's expiry (S9)

The 1s ticker's own ExpireLeases call almost always won the race against
the coordinator's 30s Monitor loop, so the coordinator's ExpireLeases saw
nothing left to expire and never reached its session-kill path — silently
orphaning herdr panes past their lease TTL whenever a coordinator is
configured. AUDIT.md called this "harmless" on the assumption CAS merely
picked a winner; the real effect is the pane-kill side effect never firing.
Now the ticker defers reclaim entirely to the coordinator when one exists,
keeping only AssignPending as a periodic retry.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W1rkJ2hBMybnJctPbcy4tT
This commit is contained in:
kami
2026-07-27 23:32:26 +04:00
parent 72f6230b4a
commit fab9225a78
+17
View File
@@ -639,6 +639,23 @@ func main() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for range ticker.C {
// When a coordinator is running, its own Monitor loop (every
// 30s) already calls Store.ExpireLeases and, critically,
// kills the herdr session for whatever it expires (S9: two
// independent expiry loops raced on the same reclaim, and
// since this one runs every second vs. the coordinator's
// 30s, it almost always won — meaning the coordinator's
// ExpireLeases call saw nothing left to expire and its
// session-kill path never ran, silently orphaning panes
// past their lease TTL). Calling ExpireLeases here too would
// just resurrect that race, so leave reclaim to the
// coordinator and only keep retrying pending assignment.
if coordinator != nil {
if _, err := rt.AssignPending(); err != nil {
log.Printf("route expired task: %v", err)
}
continue
}
if _, err := s.ExpireLeases(time.Now()); err != nil {
log.Printf("expire leases: %v", err)
} else if _, err := rt.AssignPending(); err != nil {