Files
EvoBGP/internal/pipeline/refresh_aggregate_test.go
Denozordec 07b97bddc1
CI / changes (push) Successful in 7s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 33s
CI / docker-web (push) Has been skipped
CI / docker-bird (push) Has been skipped
CI / bird2 (push) Successful in 16s
CI / docker-go (push) Successful in 8m30s
feat: enhance observability with prefix aggregation metrics
- Added new histograms to track prefix aggregation duration, raw count, and aggregated count during tenant rendering.
- Implemented `RecordPrefixAggregation` function to record metrics for aggregation performance.
- Updated `RenderTenantRevision` and `RenderTenantRevisionFromPrefixes` functions to log aggregation statistics.
- Refactored `smartAggregatePrefixRows` to support both IPv4 and IPv6 aggregation, improving overall prefix handling.
2026-05-19 15:38:33 +07:00

272 lines
8.5 KiB
Go

package pipeline
import (
"context"
"net/http"
"testing"
"evobgp/internal/store"
)
func prefixRowSet(rows []store.PrefixRow) map[string]struct{} {
got := make(map[string]struct{}, len(rows))
for _, r := range rows {
c := prefixRowCommunity(r)
got[r.Prefix+"|"+c+"|"+r.Source] = struct{}{}
}
return got
}
func TestRefreshModule_AggregatesAllEnabledModules(t *testing.T) {
t.Setenv("EVOBGP_ASN_RESOLVE", "0")
m := store.NewMemory()
m.SeedDemo()
tenant, _, modIP, _, _ := m.DemoIDs()
for _, mod := range m.ListModules(tenant) {
if mod == nil || mod.ID == modIP {
continue
}
disabled := false
if _, err := m.UpdateModule(tenant, mod.ID, &store.ModulePatch{Enabled: &disabled}); err != nil {
t.Fatal(err)
}
}
mod2, err := m.CreateModule(tenant, &store.Module{Type: "IP_RANGES", Name: "extra-ip", Enabled: true, Priority: 30})
if err != nil {
t.Fatal(err)
}
if _, err := m.CreateIPRangeEntry(tenant, modIP, &store.IPRangeEntry{Prefix: "10.0.0.0/24"}); err != nil {
t.Fatal(err)
}
if _, err := m.CreateIPRangeEntry(tenant, mod2.ID, &store.IPRangeEntry{Prefix: "192.168.0.0/24"}); err != nil {
t.Fatal(err)
}
ctx := context.Background()
before, _, _ := m.ListRevisions(tenant, "", "", 200)
beforeN := len(before)
rev1, err := RefreshModule(ctx, m, http.DefaultClient, tenant, modIP)
if err != nil {
t.Fatal(err)
}
after1, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(after1) != beforeN+1 {
t.Fatalf("first refresh: want one new tenant revision, got %d -> %d", beforeN, len(after1))
}
px, _, _ := m.ListRevisionPrefixes(tenant, rev1, "", 1000)
if len(px) != 2 {
t.Fatalf("first refresh: want 2 aggregated prefixes, got %d: %+v", len(px), px)
}
rev2, err := RefreshModule(ctx, m, http.DefaultClient, tenant, mod2.ID)
if err != nil {
t.Fatal(err)
}
if rev2 == "" {
t.Fatal("second refresh: expected non-empty revision id")
}
after2, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(after2) < len(after1) {
t.Fatalf("second refresh: revisions count should not decrease, had %d now %d", len(after1), len(after2))
}
if _, err := m.CreateIPRangeEntry(tenant, mod2.ID, &store.IPRangeEntry{Prefix: "10.0.1.0/24"}); err != nil {
t.Fatal(err)
}
rev3, err := RefreshModule(ctx, m, http.DefaultClient, tenant, mod2.ID)
if err != nil {
t.Fatal(err)
}
if rev3 == rev1 {
t.Fatal("after prefix change, expected a new revision")
}
after3, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(after3) < len(after2) {
t.Fatalf("third refresh: revisions count should not decrease, had %d now %d", len(after2), len(after3))
}
px3, _, _ := m.ListRevisionPrefixes(tenant, rev3, "", 1000)
if len(px3) != 2 {
t.Fatalf("third refresh: smart aggregation should merge adjacent /24, want 2 prefixes, got %d", len(px3))
}
}
func TestSmartAggregatePrefixRows_RespectsCommunityAndSource(t *testing.T) {
commA := "c-a"
commB := "c-b"
rows := []store.PrefixRow{
{Prefix: "10.0.0.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.1.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.2.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.3.0/24", CommunityID: &commA, Source: "ip_range"},
{Prefix: "10.0.4.0/24", CommunityID: &commB, Source: "ip_range"},
{Prefix: "10.0.5.0/24", CommunityID: &commB, Source: "cdn:x"},
}
out := smartAggregatePrefixRows(rows)
got := make(map[string]struct{}, len(out))
for _, r := range out {
c := ""
if r.CommunityID != nil {
c = *r.CommunityID
}
got[r.Prefix+"|"+c+"|"+r.Source] = struct{}{}
}
// First four /24 collapse into /22 because attributes are identical.
if _, ok := got["10.0.0.0/22|c-a|ip_range"]; !ok {
t.Fatalf("expected merged prefix for c-a/ip_range, got: %+v", out)
}
// Different community/source must stay separate.
if _, ok := got["10.0.4.0/24|c-b|ip_range"]; !ok {
t.Fatalf("expected distinct prefix for c-b/ip_range, got: %+v", out)
}
if _, ok := got["10.0.5.0/24|c-b|cdn:x"]; !ok {
t.Fatalf("expected distinct prefix for c-b/cdn:x, got: %+v", out)
}
if len(out) != 3 {
t.Fatalf("expected 3 resulting rows, got %d: %+v", len(out), out)
}
}
func TestSmartAggregatePrefixRows_PruneCoveredPrefixes(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "10.0.0.0/16", Source: "ip_range"},
{Prefix: "10.0.0.0/24", Source: "ip_range"},
{Prefix: "10.0.1.0/24", Source: "ip_range"},
}
out := smartAggregatePrefixRows(rows)
got := prefixRowSet(out)
if len(out) != 1 {
t.Fatalf("expected 1 row after covered prune, got %d: %+v", len(out), out)
}
if _, ok := got["10.0.0.0/16||ip_range"]; !ok {
t.Fatalf("expected /16 only, got: %+v", out)
}
}
func TestSmartAggregatePrefixRows_MergeChainFourSlash24(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "10.0.0.0/24", Source: "ip_range"},
{Prefix: "10.0.1.0/24", Source: "ip_range"},
{Prefix: "10.0.2.0/24", Source: "ip_range"},
{Prefix: "10.0.3.0/24", Source: "ip_range"},
}
out := smartAggregatePrefixRows(rows)
got := prefixRowSet(out)
if _, ok := got["10.0.0.0/22||ip_range"]; !ok {
t.Fatalf("expected merged /22, got: %+v", out)
}
if len(out) != 1 {
t.Fatalf("expected 1 row, got %d: %+v", len(out), out)
}
}
func TestSmartAggregatePrefixRows_DoesNotMergeAdjacentSlash8(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "10.0.0.0/8", Source: "ip_range"},
{Prefix: "11.0.0.0/8", Source: "ip_range"},
}
out := smartAggregatePrefixRows(rows)
if len(out) != 2 {
t.Fatalf("expected two /8 prefixes, got %d: %+v", len(out), out)
}
}
func TestSmartAggregatePrefixRows_MergesIPv6Slash64(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "2001:db8:0:0::/64", Source: "ip_range"},
{Prefix: "2001:db8:0:1::/64", Source: "ip_range"},
}
out := smartAggregatePrefixRows(rows)
got := prefixRowSet(out)
if _, ok := got["2001:db8::/63||ip_range"]; !ok {
t.Fatalf("expected merged IPv6 /63, got: %+v", out)
}
if len(out) != 1 {
t.Fatalf("expected 1 row, got %d: %+v", len(out), out)
}
}
func TestSmartAggregatePrefixRows_StableOutputOrder(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "192.168.0.0/24", Source: "cdn:b"},
{Prefix: "10.0.0.0/24", Source: "ip_range"},
{Prefix: "10.0.1.0/24", Source: "ip_range"},
}
out1 := smartAggregatePrefixRows(rows)
out2 := smartAggregatePrefixRows(rows)
if len(out1) != len(out2) {
t.Fatalf("length mismatch: %d vs %d", len(out1), len(out2))
}
for i := range out1 {
if out1[i].Prefix != out2[i].Prefix || out1[i].Source != out2[i].Source {
t.Fatalf("order not stable at %d: %+v vs %+v", i, out1[i], out2[i])
}
}
}
func TestRenderTenantRevisionFromPrefixes_SkipsWhenRedundantRawRows(t *testing.T) {
t.Setenv("EVOBGP_ASN_RESOLVE", "0")
m := store.NewMemory()
m.SeedDemo()
tenant, _, modIP, _, _ := m.DemoIDs()
for _, mod := range m.ListModules(tenant) {
if mod == nil || mod.ID == modIP {
continue
}
disabled := false
if _, err := m.UpdateModule(tenant, mod.ID, &store.ModulePatch{Enabled: &disabled}); err != nil {
t.Fatal(err)
}
}
ctx := context.Background()
baseRows := []store.PrefixRow{{Prefix: "10.0.0.0/16", Source: "ip_range"}}
rev1, err := RenderTenantRevisionFromPrefixes(ctx, m, http.DefaultClient, tenant, modIP, baseRows)
if err != nil {
t.Fatal(err)
}
rev2, err := RenderTenantRevisionFromPrefixes(ctx, m, http.DefaultClient, tenant, modIP, append(baseRows,
store.PrefixRow{Prefix: "10.0.0.0/24", Source: "ip_range"},
))
if err != nil {
t.Fatal(err)
}
if rev2 != rev1 {
t.Fatalf("expected hash skip for redundant covered prefix, got rev1=%s rev2=%s", rev1, rev2)
}
before, _, _ := m.ListRevisions(tenant, "", "", 200)
rev3, err := RenderTenantRevisionFromPrefixes(ctx, m, http.DefaultClient, tenant, modIP, append(baseRows,
store.PrefixRow{Prefix: "10.0.0.0/24", Source: "ip_range"},
store.PrefixRow{Prefix: "10.0.0.0/24", Source: "ip_range"},
))
if err != nil {
t.Fatal(err)
}
if rev3 != rev1 {
t.Fatalf("expected hash skip for duplicate raw rows, got rev1=%s rev3=%s", rev1, rev3)
}
after, _, _ := m.ListRevisions(tenant, "", "", 200)
if len(after) != len(before) {
t.Fatalf("duplicate raw rows should not create revision, before=%d after=%d", len(before), len(after))
}
}
func TestHashAggregatedMaterialization_DedupesIdenticalRows(t *testing.T) {
rows := []store.PrefixRow{
{Prefix: "10.0.0.0/24", Source: "ip_range"},
{Prefix: "10.0.0.0/24", Source: "ip_range"},
}
h1 := hashAggregatedMaterialization("tenant-a", rows)
h2 := hashAggregatedMaterialization("tenant-a", []store.PrefixRow{
{Prefix: "10.0.0.0/24", Source: "ip_range"},
})
if h1 != h2 {
t.Fatalf("expected deduped hash to match, got %s vs %s", h1, h2)
}
}