diff --git a/pkg/agent/bird.go b/pkg/agent/bird.go index 3a38d31..c0ce126 100644 --- a/pkg/agent/bird.go +++ b/pkg/agent/bird.go @@ -24,11 +24,18 @@ type BirdManager struct { BirdctlPath string // "birdc" — overridable for tests Logger *slog.Logger - mu sync.Mutex - last string // last rendered output (de-dup) - debounce *time.Timer + mu sync.Mutex + last string // last rendered output (de-dup) + cooldown *time.Timer + cooldownEnd time.Time // window during which further reloads are coalesced + pending bool // a render landed during cooldown; reload at window end } +// reloadCooldown is the minimum spacing between two birdc reloads. The +// first change fires immediately (no leading-edge delay); follow-up +// changes within this window are coalesced into a single tail reload. +const reloadCooldown = 500 * time.Millisecond + // Render writes the config from a NodeConfig + anycast set. Idempotent — // if the rendered content matches what we last wrote, no birdc reload. func (b *BirdManager) Render(nc *flockv1alpha1.NodeConfig, anycast6, anycast4 []string, routerID string) error { @@ -88,12 +95,32 @@ func (b *BirdManager) Render(nc *flockv1alpha1.NodeConfig, anycast6, anycast4 [] return nil } -// scheduleReload coalesces birdc reload calls into ~500ms windows. +// scheduleReload uses leading-edge + cooldown semantics: the first call +// reloads immediately; subsequent calls within reloadCooldown coalesce +// into a single deferred reload at the cooldown's end. Caller holds b.mu. func (b *BirdManager) scheduleReload() { - if b.debounce != nil { - b.debounce.Stop() + now := time.Now() + if now.After(b.cooldownEnd) { + // Outside any active cooldown — fire now (leading edge). + b.cooldownEnd = now.Add(reloadCooldown) + b.pending = false + go b.reload() + return } - b.debounce = time.AfterFunc(500*time.Millisecond, b.reload) + // Inside cooldown — coalesce. If no tail timer is set, schedule one + // at the cooldown end; if already set, just leave it. + if b.pending { + return + } + b.pending = true + delay := b.cooldownEnd.Sub(now) + b.cooldown = time.AfterFunc(delay, func() { + b.mu.Lock() + b.pending = false + b.cooldownEnd = time.Now().Add(reloadCooldown) + b.mu.Unlock() + b.reload() + }) } func (b *BirdManager) reload() {