check / check (push) Canceled after 0s
The prefix handler ran one PeerDown per peer, one after another, and stopped taking messages at 20,000 held-back messages. Sessions going down together queued up their walks, so a returning full-table peer could reach that limit. The table's PeersDown now takes several peers out of every prefix in one walk. One walk runs at a time. The next one takes every peer whose session went down meanwhile, so a message is held back for two walks at most. Each walk takes about 2.6 seconds for each full-table peer it takes out. The limit is now 150,000 messages, about 150 MiB: enough when four full-table peers go down together. The status page shows how many messages each handler holds back. Model: opus-5-5
702 lines
21 KiB
Go
702 lines
21 KiB
Go
package routetable_test
|
|
|
|
import (
|
|
"math"
|
|
"net/netip"
|
|
"reflect"
|
|
"slices"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/bits-and-blooms/bitset"
|
|
|
|
"git.eeqj.de/sneak/routewatch/internal/routetable"
|
|
)
|
|
|
|
const (
|
|
peerA = "192.0.2.1"
|
|
peerB = "192.0.2.2"
|
|
peerC = "2001:db8::3"
|
|
peerD = "2001:db8::4"
|
|
|
|
asA = 64500
|
|
asB = 64501
|
|
asC = 64502
|
|
// transitAS is the first AS of every test path, the peer's own.
|
|
transitAS = 64496
|
|
|
|
prefixV4 = "10.1.2.0/24"
|
|
prefixV6 = "2001:db8:1::/48"
|
|
)
|
|
|
|
// at returns the time second seconds after the start of every test.
|
|
func at(second int) time.Time {
|
|
return time.Date(2026, 1, 2, 3, 4, 5, 0, time.UTC).
|
|
Add(time.Duration(second) * time.Second)
|
|
}
|
|
|
|
// timeAt returns at(second) for a field that may be nil.
|
|
func timeAt(second int) *time.Time {
|
|
t := at(second)
|
|
|
|
return &t
|
|
}
|
|
|
|
// routeOf returns the route peer announces with origin AS asn second seconds
|
|
// after the start.
|
|
func routeOf(peer string, asn, second int) routetable.Route {
|
|
return routetable.Route{
|
|
Peer: peer, ASPath: []int{transitAS, asn}, Time: at(second),
|
|
}
|
|
}
|
|
|
|
// step is one update a test applies to a table for one prefix.
|
|
type step func(table *routetable.Table, prefix netip.Prefix)
|
|
|
|
// announce is the step of peer announcing prefix with origin AS asn second
|
|
// seconds after the start.
|
|
func announce(peer string, asn, second int) step {
|
|
return func(table *routetable.Table, prefix netip.Prefix) {
|
|
table.Announce(prefix, asn, routeOf(peer, asn, second))
|
|
}
|
|
}
|
|
|
|
// withdraw is the step of peer withdrawing prefix.
|
|
func withdraw(peer string) step {
|
|
return func(table *routetable.Table, prefix netip.Prefix) {
|
|
table.Withdraw(prefix, peer)
|
|
}
|
|
}
|
|
|
|
// peersDown is the step of the sessions of peers going down.
|
|
func peersDown(peers ...string) step {
|
|
return func(table *routetable.Table, _ netip.Prefix) {
|
|
table.PeersDown(peers)
|
|
}
|
|
}
|
|
|
|
// rule is one case of the rule that decides what the table holds for a prefix:
|
|
// the steps applied to an empty table, and the prefix's origins after them.
|
|
// The peers are numbered in the order they first announce, which is the order
|
|
// Origin.Peers lists them in.
|
|
type rule struct {
|
|
name string
|
|
steps []step
|
|
want []routetable.Origin
|
|
}
|
|
|
|
// checkRules runs each case for an IPv4 and an IPv6 prefix.
|
|
func checkRules(t *testing.T, rules []rule) {
|
|
t.Helper()
|
|
|
|
for _, tc := range rules {
|
|
for _, text := range []string{prefixV4, prefixV6} {
|
|
t.Run(tc.name+" "+text, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
prefix := netip.MustParsePrefix(text)
|
|
table := routetable.New(routetable.Params{})
|
|
|
|
for _, apply := range tc.steps {
|
|
apply(table, prefix)
|
|
}
|
|
|
|
got := table.Origins(prefix)
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Errorf("Origins(%s) = %+v, want %+v", prefix, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnnouncementsKeepTheTwoNewestRoutes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checkRules(t, []rule{
|
|
{
|
|
name: "an announcement is kept",
|
|
steps: []step{announce(peerA, asA, 10)},
|
|
want: []routetable.Origin{{ASN: asA, Peers: []string{peerA},
|
|
Routes: []routetable.Route{routeOf(peerA, asA, 10)}}},
|
|
},
|
|
{
|
|
name: "a lagging peer's older route is not kept",
|
|
steps: []step{announce(peerA, asA, 20), announce(peerB, asA, 30),
|
|
announce(peerC, asA, 10)},
|
|
want: []routetable.Origin{{ASN: asA,
|
|
Peers: []string{peerA, peerB, peerC}, Routes: []routetable.Route{
|
|
routeOf(peerB, asA, 30), routeOf(peerA, asA, 20),
|
|
}}},
|
|
},
|
|
{
|
|
name: "a third newer route drops the older kept one, whose peer " +
|
|
"keeps its bit",
|
|
steps: []step{announce(peerA, asA, 10), announce(peerB, asA, 20),
|
|
announce(peerC, asA, 30)},
|
|
want: []routetable.Origin{{ASN: asA,
|
|
Peers: []string{peerA, peerB, peerC}, Routes: []routetable.Route{
|
|
routeOf(peerC, asA, 30), routeOf(peerB, asA, 20),
|
|
}}},
|
|
},
|
|
{
|
|
name: "a route as old as the older kept one replaces it",
|
|
steps: []step{announce(peerA, asA, 10), announce(peerB, asA, 20),
|
|
announce(peerC, asA, 10)},
|
|
want: []routetable.Origin{{ASN: asA,
|
|
Peers: []string{peerA, peerB, peerC}, Routes: []routetable.Route{
|
|
routeOf(peerB, asA, 20), routeOf(peerC, asA, 10),
|
|
}}},
|
|
},
|
|
{
|
|
name: "a peer's new route replaces its kept one",
|
|
steps: []step{announce(peerA, asA, 10), announce(peerB, asA, 20),
|
|
announce(peerA, asA, 30)},
|
|
want: []routetable.Origin{{ASN: asA, Peers: []string{peerA, peerB},
|
|
Routes: []routetable.Route{
|
|
routeOf(peerA, asA, 30), routeOf(peerB, asA, 20),
|
|
}}},
|
|
},
|
|
{
|
|
name: "a peer's new route replaces its kept one even when older",
|
|
steps: []step{announce(peerA, asA, 10), announce(peerB, asA, 20),
|
|
announce(peerB, asA, 5)},
|
|
want: []routetable.Origin{{ASN: asA, Peers: []string{peerA, peerB},
|
|
Routes: []routetable.Route{
|
|
routeOf(peerA, asA, 10), routeOf(peerB, asA, 5),
|
|
}}},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestAPeerCarriesAPrefixWithOneOrigin(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
checkRules(t, []rule{
|
|
{
|
|
name: "a peer moving to a new origin leaves the old one",
|
|
steps: []step{announce(peerA, asA, 10), announce(peerB, asA, 20),
|
|
announce(peerA, asB, 30)},
|
|
want: []routetable.Origin{
|
|
{ASN: asA, Peers: []string{peerB},
|
|
Routes: []routetable.Route{routeOf(peerB, asA, 20)}},
|
|
{ASN: asB, Peers: []string{peerA},
|
|
Routes: []routetable.Route{routeOf(peerA, asB, 30)}},
|
|
},
|
|
},
|
|
{
|
|
name: "a peer whose route is not kept moves to a new origin",
|
|
steps: []step{announce(peerA, asA, 10), announce(peerB, asA, 20),
|
|
announce(peerC, asA, 30), announce(peerA, asB, 40)},
|
|
want: []routetable.Origin{
|
|
{ASN: asA, Peers: []string{peerB, peerC},
|
|
Routes: []routetable.Route{
|
|
routeOf(peerC, asA, 30), routeOf(peerB, asA, 20),
|
|
}},
|
|
{ASN: asB, Peers: []string{peerA},
|
|
Routes: []routetable.Route{routeOf(peerA, asB, 40)}},
|
|
},
|
|
},
|
|
{
|
|
name: "origins are listed in AS number order",
|
|
steps: []step{announce(peerA, asC, 10), announce(peerB, asA, 20),
|
|
announce(peerC, asB, 30)},
|
|
want: []routetable.Origin{
|
|
{ASN: asA, Peers: []string{peerB},
|
|
Routes: []routetable.Route{routeOf(peerB, asA, 20)}},
|
|
{ASN: asB, Peers: []string{peerC},
|
|
Routes: []routetable.Route{routeOf(peerC, asB, 30)}},
|
|
{ASN: asC, Peers: []string{peerA},
|
|
Routes: []routetable.Route{routeOf(peerA, asC, 10)}},
|
|
},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestWithdrawalsAndPeerDowns(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// threePeers is peerA, peerB and peerC announcing in turn; the routes of
|
|
// peerC and peerB are kept.
|
|
threePeers := []step{announce(peerA, asA, 10), announce(peerB, asA, 20),
|
|
announce(peerC, asA, 30)}
|
|
|
|
checkRules(t, []rule{
|
|
{
|
|
name: "a withdrawal drops the peer's kept route",
|
|
steps: append(slices.Clone(threePeers), withdraw(peerC)),
|
|
want: []routetable.Origin{{ASN: asA, Peers: []string{peerA, peerB},
|
|
Routes: []routetable.Route{routeOf(peerB, asA, 20)}}},
|
|
},
|
|
{
|
|
name: "a withdrawal of a route that is not kept leaves the kept ones",
|
|
steps: append(slices.Clone(threePeers), withdraw(peerA)),
|
|
want: []routetable.Origin{{ASN: asA, Peers: []string{peerB, peerC},
|
|
Routes: []routetable.Route{
|
|
routeOf(peerC, asA, 30), routeOf(peerB, asA, 20),
|
|
}}},
|
|
},
|
|
{
|
|
name: "an origin whose kept routes are withdrawn keeps its other " +
|
|
"peers",
|
|
steps: append(slices.Clone(threePeers), withdraw(peerB),
|
|
withdraw(peerC)),
|
|
want: []routetable.Origin{{ASN: asA, Peers: []string{peerA}}},
|
|
},
|
|
{
|
|
name: "an origin without kept routes keeps the next update",
|
|
steps: append(slices.Clone(threePeers), withdraw(peerB),
|
|
withdraw(peerC), announce(peerA, asA, 5)),
|
|
want: []routetable.Origin{{ASN: asA, Peers: []string{peerA},
|
|
Routes: []routetable.Route{routeOf(peerA, asA, 5)}}},
|
|
},
|
|
{
|
|
name: "an origin no peer carries is removed",
|
|
steps: []step{announce(peerA, asA, 10), announce(peerB, asB, 20),
|
|
withdraw(peerA)},
|
|
want: []routetable.Origin{{ASN: asB, Peers: []string{peerB},
|
|
Routes: []routetable.Route{routeOf(peerB, asB, 20)}}},
|
|
},
|
|
{
|
|
name: "a prefix no peer carries is removed",
|
|
steps: []step{announce(peerA, asA, 10), withdraw(peerA)},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "a withdrawal from a peer that does not carry it changes nothing",
|
|
steps: []step{announce(peerA, asA, 10), withdraw(peerB)},
|
|
want: []routetable.Origin{{ASN: asA, Peers: []string{peerA},
|
|
Routes: []routetable.Route{routeOf(peerA, asA, 10)}}},
|
|
},
|
|
{
|
|
name: "a peer going down leaves the prefix",
|
|
steps: []step{announce(peerA, asA, 10), announce(peerB, asA, 20),
|
|
peersDown(peerB)},
|
|
want: []routetable.Origin{{ASN: asA, Peers: []string{peerA},
|
|
Routes: []routetable.Route{routeOf(peerA, asA, 10)}}},
|
|
},
|
|
{
|
|
name: "a peer going down after it withdrew changes nothing",
|
|
steps: []step{announce(peerA, asA, 10), announce(peerB, asA, 20),
|
|
withdraw(peerB), peersDown(peerB)},
|
|
want: []routetable.Origin{{ASN: asA, Peers: []string{peerA},
|
|
Routes: []routetable.Route{routeOf(peerA, asA, 10)}}},
|
|
},
|
|
{
|
|
name: "peers going down together each leave the prefix",
|
|
steps: []step{announce(peerA, asA, 10), announce(peerB, asB, 20),
|
|
announce(peerC, asB, 30), peersDown(peerA, peerB)},
|
|
want: []routetable.Origin{{ASN: asB, Peers: []string{peerC},
|
|
Routes: []routetable.Route{routeOf(peerC, asB, 30)}}},
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestPeersDownLeavesEveryPrefix(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
table := routetable.New(routetable.Params{})
|
|
prefixes := []netip.Prefix{
|
|
netip.MustParsePrefix(prefixV4),
|
|
netip.MustParsePrefix("10.2.0.0/16"),
|
|
netip.MustParsePrefix(prefixV6),
|
|
}
|
|
|
|
// peerA carries every prefix with origin asA, peerB the first one with
|
|
// origin asB.
|
|
for _, prefix := range prefixes {
|
|
table.Announce(prefix, asA, routeOf(peerA, asA, 10))
|
|
}
|
|
|
|
table.Announce(prefixes[0], asB, routeOf(peerB, asB, 20))
|
|
table.PeersDown([]string{peerA})
|
|
|
|
want := []routetable.Origin{{ASN: asB, Peers: []string{peerB},
|
|
Routes: []routetable.Route{routeOf(peerB, asB, 20)}}}
|
|
|
|
got := table.Origins(prefixes[0])
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("Origins(%s) = %+v, want %+v", prefixes[0], got, want)
|
|
}
|
|
|
|
for _, prefix := range prefixes[1:] {
|
|
got := table.Origins(prefix)
|
|
if got != nil {
|
|
t.Errorf("Origins(%s) = %+v after the only peer went down, want nil",
|
|
prefix, got)
|
|
}
|
|
}
|
|
|
|
counts := table.Counts()
|
|
if counts.IPv4Prefixes != 1 || counts.IPv6Prefixes != 0 ||
|
|
counts.IPv4Routes != 1 || counts.IPv6Routes != 0 {
|
|
t.Errorf("Counts() = %+v, want one IPv4 prefix and route", counts)
|
|
}
|
|
|
|
if len(table.OriginatedPrefixes(asA)) != 0 {
|
|
t.Errorf("OriginatedPrefixes(%d) = %+v, want none",
|
|
asA, table.OriginatedPrefixes(asA))
|
|
}
|
|
}
|
|
|
|
// TestPeersDownTakesOutPeersOfDifferentPrefixes takes peerA and peerB out in
|
|
// one PeersDown. peerA carries only prefixV4, and peerB only prefixV6. So
|
|
// peerA's leaving removes prefixV4 before PeersDown looks for peerB there. Both
|
|
// prefixes must then be gone.
|
|
func TestPeersDownTakesOutPeersOfDifferentPrefixes(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
table := routetable.New(routetable.Params{})
|
|
prefixes := []netip.Prefix{
|
|
netip.MustParsePrefix(prefixV4),
|
|
netip.MustParsePrefix(prefixV6),
|
|
}
|
|
|
|
table.Announce(prefixes[0], asA, routeOf(peerA, asA, 10))
|
|
table.Announce(prefixes[1], asB, routeOf(peerB, asB, 20))
|
|
table.PeersDown([]string{peerA, peerB})
|
|
|
|
for _, prefix := range prefixes {
|
|
got := table.Origins(prefix)
|
|
if got != nil {
|
|
t.Errorf("Origins(%s) = %+v after its only peer went down, want nil",
|
|
prefix, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCountsOriginsWithoutKeptRoute(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
prefix := netip.MustParsePrefix(prefixV4)
|
|
table := routetable.New(routetable.Params{})
|
|
|
|
steps := []struct {
|
|
apply step
|
|
want int
|
|
}{
|
|
{announce(peerA, asA, 10), 0},
|
|
{announce(peerB, asA, 20), 0},
|
|
{announce(peerC, asA, 30), 0},
|
|
{withdraw(peerC), 0},
|
|
{withdraw(peerB), 1},
|
|
// An update from the last peer is kept.
|
|
{announce(peerA, asA, 40), 0},
|
|
{withdraw(peerA), 0},
|
|
// Once more without kept routes, then without peers: the origin
|
|
// is removed.
|
|
{announce(peerA, asA, 10), 0},
|
|
{announce(peerB, asA, 20), 0},
|
|
{announce(peerC, asA, 30), 0},
|
|
{withdraw(peerB), 0},
|
|
{withdraw(peerC), 1},
|
|
{peersDown(peerA), 0},
|
|
}
|
|
|
|
for i, s := range steps {
|
|
s.apply(table, prefix)
|
|
|
|
got := table.Counts().OriginsWithoutKeptRoute
|
|
if got != s.want {
|
|
t.Errorf("after step %d, OriginsWithoutKeptRoute = %d, want %d",
|
|
i, got, s.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// change is a routetable.Change with its peer set as a list of peer numbers.
|
|
type change struct {
|
|
Prefix string
|
|
OriginASN int
|
|
Removed bool
|
|
Peers []uint
|
|
Routes []routetable.Route
|
|
}
|
|
|
|
// peerNumbers returns the numbers in set, or nil for a nil set.
|
|
func peerNumbers(set *bitset.BitSet) []uint {
|
|
if set == nil {
|
|
return nil
|
|
}
|
|
|
|
return slices.Collect(set.EachSet())
|
|
}
|
|
|
|
func TestTakeChanges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// peerA was numbered 5 before; the next new peer is 6.
|
|
table := routetable.New(routetable.Params{
|
|
PeerNumbers: map[string]uint{peerA: 5},
|
|
})
|
|
v4 := netip.MustParsePrefix(prefixV4)
|
|
v6 := netip.MustParsePrefix(prefixV6)
|
|
|
|
table.Announce(v6, asA, routeOf(peerA, asA, 10))
|
|
table.Announce(v4, asA, routeOf(peerA, asA, 10))
|
|
table.Announce(v4, asA, routeOf(peerB, asA, 20))
|
|
|
|
checkChanges(t, table, []change{
|
|
{Prefix: prefixV4, OriginASN: asA, Peers: []uint{5, 6},
|
|
Routes: []routetable.Route{
|
|
routeOf(peerB, asA, 20), routeOf(peerA, asA, 10),
|
|
}},
|
|
{Prefix: prefixV6, OriginASN: asA, Peers: []uint{5},
|
|
Routes: []routetable.Route{routeOf(peerA, asA, 10)}},
|
|
}, map[string]uint{peerB: 6})
|
|
|
|
// TakeChanges cleared the record.
|
|
checkChanges(t, table, []change{}, map[string]uint{})
|
|
|
|
// peerB moves to asB; peerA withdraws, which removes the pair of v4 and
|
|
// asA.
|
|
table.Announce(v4, asB, routeOf(peerB, asB, 30))
|
|
table.Withdraw(v4, peerA)
|
|
|
|
checkChanges(t, table, []change{
|
|
{Prefix: prefixV4, OriginASN: asA, Removed: true},
|
|
{Prefix: prefixV4, OriginASN: asB, Peers: []uint{6},
|
|
Routes: []routetable.Route{routeOf(peerB, asB, 30)}},
|
|
}, map[string]uint{})
|
|
}
|
|
|
|
func TestTakeChangesHoldsOnlyPairsThatChanged(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
table := routetable.New(routetable.Params{})
|
|
v4 := netip.MustParsePrefix(prefixV4)
|
|
v6 := netip.MustParsePrefix(prefixV6)
|
|
|
|
// peerA, peerB and peerC are numbered 0, 1 and 2. The routes of peerC and
|
|
// peerB to v4 are kept; peerA keeps only its bit.
|
|
announceAll(table, prefixV4, asA, 10, peerA, peerB, peerC)
|
|
table.Announce(v6, asA, routeOf(peerB, asA, 10))
|
|
table.TakeChanges(math.MaxInt)
|
|
|
|
// peerA announces again, with a time older than both kept routes.
|
|
table.Announce(v4, asA, routeOf(peerA, asA, 15))
|
|
checkChanges(t, table, []change{}, map[string]uint{})
|
|
|
|
// peerC announces its kept route again.
|
|
table.Announce(v4, asA, routeOf(peerC, asA, 30))
|
|
checkChanges(t, table, []change{}, map[string]uint{})
|
|
|
|
// peerC announces the same path with a newer time, so its kept route
|
|
// changes.
|
|
table.Announce(v4, asA, routeOf(peerC, asA, 40))
|
|
checkChanges(t, table, []change{
|
|
{Prefix: prefixV4, OriginASN: asA, Peers: []uint{0, 1, 2},
|
|
Routes: []routetable.Route{
|
|
routeOf(peerC, asA, 40), routeOf(peerB, asA, 20),
|
|
}},
|
|
}, map[string]uint{})
|
|
|
|
// peerA withdraws v6, which it does not carry.
|
|
table.Withdraw(v6, peerA)
|
|
checkChanges(t, table, []change{}, map[string]uint{})
|
|
|
|
// peerA going down changes v4, the one prefix it carries.
|
|
table.PeersDown([]string{peerA})
|
|
checkChanges(t, table, []change{
|
|
{Prefix: prefixV4, OriginASN: asA, Peers: []uint{1, 2},
|
|
Routes: []routetable.Route{
|
|
routeOf(peerC, asA, 40), routeOf(peerB, asA, 20),
|
|
}},
|
|
}, map[string]uint{})
|
|
|
|
// peerA, which carries nothing now, goes down again.
|
|
table.PeersDown([]string{peerA})
|
|
checkChanges(t, table, []change{}, map[string]uint{})
|
|
}
|
|
|
|
// TestTakeChangesTakesAtMostMaxChanges changes three prefix and origin AS
|
|
// pairs, then takes the changes two at a time. The first call must return two
|
|
// pairs and both new peers, and the second the third pair and no peer.
|
|
func TestTakeChangesTakesAtMostMaxChanges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const maxChanges = 2
|
|
|
|
table := routetable.New(routetable.Params{})
|
|
v4 := netip.MustParsePrefix(prefixV4)
|
|
v6 := netip.MustParsePrefix(prefixV6)
|
|
|
|
table.Announce(v4, asA, routeOf(peerA, asA, 10))
|
|
table.Announce(v4, asB, routeOf(peerB, asB, 10))
|
|
table.Announce(v6, asA, routeOf(peerA, asA, 10))
|
|
|
|
first, firstPeers := table.TakeChanges(maxChanges)
|
|
second, secondPeers := table.TakeChanges(maxChanges)
|
|
|
|
if len(first) != maxChanges || len(second) != 1 {
|
|
t.Fatalf("TakeChanges(%d) returned %d and then %d pairs, want %d and 1",
|
|
maxChanges, len(first), len(second), maxChanges)
|
|
}
|
|
|
|
wantPeers := map[string]uint{peerA: 0, peerB: 1}
|
|
if !reflect.DeepEqual(firstPeers, wantPeers) || len(secondPeers) != 0 {
|
|
t.Errorf("TakeChanges(%d) returned the new peers %v and then %v, "+
|
|
"want %v and none", maxChanges, firstPeers, secondPeers, wantPeers)
|
|
}
|
|
|
|
for _, c := range first {
|
|
if c.Prefix == second[0].Prefix && c.OriginASN == second[0].OriginASN {
|
|
t.Errorf("both calls returned %s with AS%d", c.Prefix, c.OriginASN)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestPutBackChanges takes a table's changes and puts them back, as a caller
|
|
// does whose write failed. The next TakeChanges must return them again, with
|
|
// each pair's content then, and with the changes since.
|
|
func TestPutBackChanges(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
table := routetable.New(routetable.Params{})
|
|
v4 := netip.MustParsePrefix(prefixV4)
|
|
v6 := netip.MustParsePrefix(prefixV6)
|
|
|
|
table.Announce(v4, asA, routeOf(peerA, asA, 10))
|
|
table.Announce(v6, asA, routeOf(peerA, asA, 10))
|
|
table.PutBackChanges(table.TakeChanges(math.MaxInt))
|
|
|
|
if got := table.ChangeCount(); got != 2 {
|
|
t.Errorf("ChangeCount() = %d after the changes were put back, want 2",
|
|
got)
|
|
}
|
|
|
|
// peerA withdraws v4, so v4's earlier content must not come back; peerB
|
|
// announces v6, and is a new peer as well.
|
|
table.Withdraw(v4, peerA)
|
|
table.Announce(v6, asA, routeOf(peerB, asA, 20))
|
|
|
|
checkChanges(t, table, []change{
|
|
{Prefix: prefixV4, OriginASN: asA, Removed: true},
|
|
{Prefix: prefixV6, OriginASN: asA, Peers: []uint{0, 1},
|
|
Routes: []routetable.Route{
|
|
routeOf(peerB, asA, 20), routeOf(peerA, asA, 10),
|
|
}},
|
|
}, map[string]uint{peerA: 0, peerB: 1})
|
|
|
|
if got := table.ChangeCount(); got != 0 {
|
|
t.Errorf("ChangeCount() = %d after TakeChanges, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestLoad(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const otherV4 = "10.9.0.0/16"
|
|
|
|
v4 := netip.MustParsePrefix(prefixV4)
|
|
v6 := netip.MustParsePrefix(prefixV6)
|
|
other := netip.MustParsePrefix(otherV4)
|
|
|
|
// The routes of peerC and peerB to v4 are kept; peerA keeps only its bit.
|
|
// The two routes to v6 have the same time. The routes of other were
|
|
// withdrawn while peerA still carries it.
|
|
source := routetable.New(routetable.Params{})
|
|
announceAll(source, prefixV4, asA, 10, peerA, peerB, peerC)
|
|
announceAll(source, prefixV4, asB, 40, peerD)
|
|
source.Announce(v6, asA, routeOf(peerA, asA, 50))
|
|
source.Announce(v6, asA, routeOf(peerC, asA, 50))
|
|
announceAll(source, otherV4, asB, 10, peerA, peerB, peerC)
|
|
source.Withdraw(other, peerB)
|
|
source.Withdraw(other, peerC)
|
|
|
|
changes, peers := source.TakeChanges(math.MaxInt)
|
|
loaded := routetable.New(routetable.Params{PeerNumbers: peers})
|
|
|
|
// Loading a pair twice changes nothing.
|
|
for range 2 {
|
|
for _, change := range changes {
|
|
loaded.Load(change)
|
|
}
|
|
}
|
|
|
|
for _, prefix := range []netip.Prefix{v4, v6, other} {
|
|
got, want := loaded.Origins(prefix), source.Origins(prefix)
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("Origins(%s) = %+v after the load, want %+v", prefix, got, want)
|
|
}
|
|
}
|
|
|
|
if !reflect.DeepEqual(loaded.Counts(), source.Counts()) {
|
|
t.Errorf("Counts() = %+v after the load, want %+v",
|
|
loaded.Counts(), source.Counts())
|
|
}
|
|
|
|
checkChanges(t, loaded, []change{}, map[string]uint{})
|
|
}
|
|
|
|
// TestConcurrentUse has goroutines update one table while others read it. The
|
|
// race detector, which make test runs, fails it when a method uses the table
|
|
// without its lock.
|
|
func TestConcurrentUse(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
// rounds is how many times each goroutine calls its method.
|
|
rounds = 1000
|
|
ipVersion = 4
|
|
sampleSize = 10
|
|
)
|
|
|
|
table := routetable.New(routetable.Params{})
|
|
prefix := netip.MustParsePrefix(prefixV4)
|
|
|
|
uses := []func(round int){
|
|
func(round int) { table.Announce(prefix, asA, routeOf(peerA, asA, round)) },
|
|
func(round int) { table.Announce(prefix, asB, routeOf(peerB, asB, round)) },
|
|
func(int) { table.Withdraw(prefix, peerA) },
|
|
func(int) { table.PeersDown([]string{peerB}) },
|
|
func(int) { table.PutBackChanges(table.TakeChanges(math.MaxInt)) },
|
|
func(int) { table.ChangeCount() },
|
|
func(int) { table.Lookup(prefix.Addr()) },
|
|
func(int) { table.Origins(prefix) },
|
|
func(int) { table.OriginatedPrefixes(asA) },
|
|
func(int) { table.SamplePrefixes(ipVersion, prefix.Bits(), sampleSize) },
|
|
func(int) { table.Counts() },
|
|
func(int) { table.Overview(sampleSize) },
|
|
}
|
|
|
|
var wg sync.WaitGroup
|
|
for _, use := range uses {
|
|
wg.Go(func() {
|
|
for round := range rounds {
|
|
use(round)
|
|
}
|
|
})
|
|
}
|
|
|
|
wg.Wait()
|
|
}
|
|
|
|
// checkChanges checks that TakeChanges, taking every change, returns want and
|
|
// wantPeers.
|
|
func checkChanges(
|
|
t *testing.T, table *routetable.Table, want []change,
|
|
wantPeers map[string]uint,
|
|
) {
|
|
t.Helper()
|
|
|
|
changes, newPeers := table.TakeChanges(math.MaxInt)
|
|
|
|
got := make([]change, 0, len(changes))
|
|
for _, c := range changes {
|
|
got = append(got, change{
|
|
Prefix: c.Prefix.String(), OriginASN: c.OriginASN, Removed: c.Removed,
|
|
Peers: peerNumbers(c.Peers), Routes: c.Routes,
|
|
})
|
|
}
|
|
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Errorf("TakeChanges() changes = %+v, want %+v", got, want)
|
|
}
|
|
|
|
if !reflect.DeepEqual(newPeers, wantPeers) {
|
|
t.Errorf("TakeChanges() new peers = %v, want %v", newPeers, wantPeers)
|
|
}
|
|
}
|