bird: leading-edge reload + 500ms cooldown (was trailing 500ms debounce)
Build flock Image / build (push) Has been cancelled

A single Ready/NotReady transition no longer pays a 500ms reload wait —
the first call to scheduleReload fires birdc immediately; further calls
within 500ms are coalesced into one tail reload at the cooldown's end.

Burst behavior is the same as before: under heavy churn (deploy rolling
all replicas at once), at most one reload per 500ms.

Steady-state latency from pod Ready transition to crt001 BGP withdraw:
  - probe period (set in pod spec, 1s minimum)
  - ~ms informer + reconcile + birdc + BGP UPDATE
The 500ms hardcoded delay is gone.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Donavan Fritz
2026-04-25 08:26:34 -05:00
parent 3117d00210
commit 677aec2a42
+34 -7
View File
@@ -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() {