Files

162 lines
3.7 KiB
Go

package main
import (
"context"
"time"
"github.com/vishvananda/netlink"
)
const (
// debounce coalesces bursts of route updates into one reconcile
debounce = 250 * time.Millisecond
minBackoff = time.Second
maxBackoff = 30 * time.Second
// Receive buffer of the subscription, so bursts of route updates (e.g. a
// full table being loaded) do not overflow it while a reconcile is running.
// An overflow closes the subscription.
subscribeBufferSize = 4 << 20
updatesQueueSize = 4096
)
// backoff doubles the delay on every failure up to maxBackoff.
type backoff time.Duration
func (b *backoff) next() time.Duration {
d := min(max(time.Duration(*b)*2, minBackoff), maxBackoff)
*b = backoff(d)
return d
}
func (b *backoff) reset() { *b = 0 }
// subscribeRoutes subscribes to route updates. Closing done ends the subscription.
func subscribeRoutes() (<-chan netlink.RouteUpdate, chan struct{}, error) {
updates := make(chan netlink.RouteUpdate, updatesQueueSize)
done := make(chan struct{})
err := netlink.RouteSubscribeWithOptions(updates, done, netlink.RouteSubscribeOptions{
ErrorCallback: func(err error) {
select {
case <-done: // closing the socket makes the receive fail
default:
logger.Warn.Printf("Route-subscription: %s", err)
}
},
ReceiveBufferSize: subscribeBufferSize,
ReceiveBufferForceSize: true,
})
if err != nil {
close(done)
return nil, nil, err
}
return updates, done, nil
}
// run reconciles until the context ends. A reconcile happens
// - after route updates of the interface, debounced,
// - every periodic if it is set,
// - after (re)subscribing, as updates may have been missed,
// - after a failure, with backoff.
func (s *syncer) run(ctx context.Context, periodic time.Duration) {
var ticks <-chan time.Time
if periodic > 0 {
ticker := time.NewTicker(periodic)
defer ticker.Stop()
ticks = ticker.C
}
var (
updates <-chan netlink.RouteUpdate // nil without subscription
done chan struct{}
// dirty: the state has to be reconciled
// recheck: updates of other interfaces came in, one of them may be a recreated interface
// armed: the work timer is running
dirty, recheck, armed = true, false, true
reconcileBackoff, subscribeBackoff backoff
)
work := time.NewTimer(0)
resubscribe := time.NewTimer(0)
arm := func() {
if !armed {
work.Reset(debounce)
armed = true
}
}
trigger := func() {
dirty = true
arm()
}
unsubscribe := func() {
if done != nil {
close(done)
}
updates, done = nil, nil
}
defer unsubscribe()
for {
select {
case <-ctx.Done():
return
case <-resubscribe.C:
var err error
updates, done, err = s.subscribe()
if err != nil {
delay := subscribeBackoff.next()
logger.Error.Printf("Couldn't subscribe to route-changes, retrying in %s: %s", delay, err)
resubscribe.Reset(delay)
continue
}
subscribeBackoff.reset()
trigger()
case update, ok := <-updates:
if !ok {
unsubscribe()
delay := subscribeBackoff.next()
logger.Warn.Printf("Route-subscription closed, resubscribing in %s", delay)
resubscribe.Reset(delay)
continue
}
if s.watches(update.Route) {
trigger()
} else {
recheck = true
arm()
}
case <-ticks:
logger.Info.Printf("Periodically syncing all routes")
trigger()
case <-work.C:
armed = false
if recheck && s.linkChanged() {
dirty = true
}
recheck = false
if !dirty {
continue
}
if err := s.reconcile(); err != nil {
delay := reconcileBackoff.next()
logger.Error.Printf("Sync failed, retrying in %s: %s", delay, err)
work.Reset(delay)
armed = true
continue
}
dirty = false
reconcileBackoff.reset()
}
}
}