Files
akvorado/outlet/metadata/cache_test.go
Vincent Bernat e2f1df9add tests: replace godebug by go-cmp for structure diffs
go-cmp is stricter and allow to catch more problems. Moreover, the
output is a bit nicer.
2025-08-23 16:03:09 +02:00

503 lines
14 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package metadata
import (
"errors"
"fmt"
"io/fs"
"math"
"math/rand"
"net/netip"
"path/filepath"
"slices"
"strconv"
"sync"
"sync/atomic"
"testing"
"time"
"akvorado/common/helpers"
"akvorado/common/reporter"
"akvorado/outlet/metadata/provider"
)
func setupTestCache(t *testing.T) (*reporter.Reporter, *metadataCache) {
t.Helper()
r := reporter.NewMock(t)
sc := newMetadataCache(r)
return r, sc
}
func expectCacheLookup(t *testing.T, sc *metadataCache, exporterIP string, ifIndex uint, expected provider.Answer) {
t.Helper()
ip := netip.MustParseAddr(exporterIP)
ip = netip.AddrFrom16(ip.As16())
got, ok := sc.Lookup(time.Time{}, provider.Query{
ExporterIP: ip,
IfIndex: ifIndex,
})
if ok && (got == provider.Answer{}) {
t.Error("Lookup() returned an empty result")
} else if !ok && (got != provider.Answer{}) {
t.Error("Lookup() returned a non-empty result")
}
if diff := helpers.Diff(got, expected); diff != "" {
t.Errorf("Lookup() (-got, +want):\n%s", diff)
}
}
func TestGetEmpty(t *testing.T) {
r, sc := setupTestCache(t)
expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{})
gotMetrics := r.GetMetrics("akvorado_outlet_metadata_cache_")
expectedMetrics := map[string]string{
`expired_entries_total`: "0",
`hits_total`: "0",
`misses_total`: "1",
`size_entries`: "0",
}
if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
t.Fatalf("Metrics (-got, +want):\n%s", diff)
}
}
func TestSimpleLookup(t *testing.T) {
r, sc := setupTestCache(t)
sc.Put(time.Now(),
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 676,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit", Speed: 1000},
})
expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit", Speed: 1000},
})
expectCacheLookup(t, sc, "127.0.0.1", 787, provider.Answer{})
expectCacheLookup(t, sc, "127.0.0.2", 676, provider.Answer{})
gotMetrics := r.GetMetrics("akvorado_outlet_metadata_cache_")
expectedMetrics := map[string]string{
`expired_entries_total`: "0",
`hits_total`: "1",
`misses_total`: "2",
`size_entries`: "1",
}
if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
t.Fatalf("Metrics (-got, +want):\n%s", diff)
}
}
func TestExpire(t *testing.T) {
r, sc := setupTestCache(t)
now := time.Now()
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 676,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
})
now = now.Add(10 * time.Minute)
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 678,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost2"},
Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
})
now = now.Add(10 * time.Minute)
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.2"),
IfIndex: 678,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost3"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
})
now = now.Add(10 * time.Minute)
sc.Expire(now.Add(-time.Hour))
expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
})
expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{
Exporter: provider.Exporter{Name: "localhost2"},
Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
})
expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{
Exporter: provider.Exporter{Name: "localhost3"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
})
sc.Expire(now.Add(-29 * time.Minute))
expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{})
expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{
Exporter: provider.Exporter{Name: "localhost2"},
Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
})
expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{
Exporter: provider.Exporter{Name: "localhost3"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
})
sc.Expire(now.Add(-19 * time.Minute))
expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{})
expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{})
expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{
Exporter: provider.Exporter{Name: "localhost3"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
})
sc.Expire(now.Add(-9 * time.Minute))
expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{})
expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{})
expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{})
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 676,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
})
now = now.Add(10 * time.Minute)
sc.Expire(now.Add(-19 * time.Minute))
expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
})
gotMetrics := r.GetMetrics("akvorado_outlet_metadata_cache_")
expectedMetrics := map[string]string{
`expired_entries_total`: "3",
`hits_total`: "7",
`misses_total`: "6",
`size_entries`: "1",
}
if diff := helpers.Diff(gotMetrics, expectedMetrics); diff != "" {
t.Fatalf("Metrics (-got, +want):\n%s", diff)
}
}
func TestExpireRefresh(t *testing.T) {
_, sc := setupTestCache(t)
now := time.Now()
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 676,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
})
now = now.Add(10 * time.Minute)
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 678,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
})
now = now.Add(10 * time.Minute)
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.2"),
IfIndex: 678,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost2"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
})
now = now.Add(10 * time.Minute)
// Refresh first entry
sc.Lookup(now, provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 676,
})
now = now.Add(10 * time.Minute)
sc.Expire(now.Add(-29 * time.Minute))
expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
})
expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{})
expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{
Exporter: provider.Exporter{Name: "localhost2"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
})
}
func TestNeedUpdates(t *testing.T) {
_, sc := setupTestCache(t)
now := time.Now()
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 676,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
})
now = now.Add(10 * time.Minute)
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 678,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
})
now = now.Add(10 * time.Minute)
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.2"),
IfIndex: 678,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost2"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX"},
})
now = now.Add(10 * time.Minute)
// Refresh
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 676,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost1"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
})
now = now.Add(10 * time.Minute)
cases := []struct {
Minutes time.Duration
Expected map[netip.Addr][]uint
}{
{9, map[netip.Addr][]uint{
netip.MustParseAddr("::ffff:127.0.0.1"): {
676,
678,
},
netip.MustParseAddr("::ffff:127.0.0.2"): {
678,
},
}},
{19, map[netip.Addr][]uint{
netip.MustParseAddr("::ffff:127.0.0.1"): {
678,
},
netip.MustParseAddr("::ffff:127.0.0.2"): {
678,
},
}},
{29, map[netip.Addr][]uint{
netip.MustParseAddr("::ffff:127.0.0.1"): {
678,
},
}},
{39, map[netip.Addr][]uint{}},
}
for _, tc := range cases {
t.Run(fmt.Sprintf("%d minutes", tc.Minutes), func(t *testing.T) {
got := sc.NeedUpdates(now.Add(-tc.Minutes * time.Minute))
// sort output for comparison purposes
for _, v := range got {
slices.Sort(v)
}
if diff := helpers.Diff(got, tc.Expected); diff != "" {
t.Fatalf("WouldExpire(%d minutes) (-got, +want):\n%s", tc.Minutes, diff)
}
})
}
}
func TestLoadNotExist(t *testing.T) {
_, sc := setupTestCache(t)
err := sc.Load("/i/do/not/exist")
if !errors.Is(err, fs.ErrNotExist) {
t.Fatalf("sc.Load() error:\n%s", err)
}
}
func TestSaveLoad(t *testing.T) {
_, sc := setupTestCache(t)
now := time.Now()
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 676,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
})
now = now.Add(10 * time.Minute)
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.1"),
IfIndex: 678,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
})
now = now.Add(10 * time.Minute)
sc.Put(now,
provider.Query{
ExporterIP: netip.MustParseAddr("::ffff:127.0.0.2"),
IfIndex: 678,
},
provider.Answer{
Exporter: provider.Exporter{Name: "localhost2"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX", Speed: 1000},
})
target := filepath.Join(t.TempDir(), "cache")
if err := sc.Save(target); err != nil {
t.Fatalf("sc.Save() error:\n%s", err)
}
_, sc = setupTestCache(t)
now = now.Add(10 * time.Minute)
if err := sc.Load(target); err != nil {
t.Fatalf("sc.Load() error:\n%s", err)
}
sc.Expire(now.Add(-29 * time.Minute))
expectCacheLookup(t, sc, "127.0.0.1", 676, provider.Answer{})
expectCacheLookup(t, sc, "127.0.0.1", 678, provider.Answer{
Exporter: provider.Exporter{Name: "localhost"},
Interface: provider.Interface{Name: "Gi0/0/0/2", Description: "Peering"},
})
expectCacheLookup(t, sc, "127.0.0.2", 678, provider.Answer{
Exporter: provider.Exporter{Name: "localhost2"},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "IX", Speed: 1000},
})
}
func TestConcurrentOperations(t *testing.T) {
r, sc := setupTestCache(t)
now := time.Now()
done := make(chan bool)
var wg sync.WaitGroup
var nowLock sync.RWMutex
// Make the clock go forward
wg.Add(1)
go func() {
defer wg.Done()
for {
nowLock.Lock()
now = now.Add(1 * time.Minute)
nowLock.Unlock()
select {
case <-done:
return
case <-time.After(1 * time.Millisecond):
}
}
}()
for range 5 {
wg.Add(1)
go func() {
defer wg.Done()
for {
nowLock.RLock()
now := now
nowLock.RUnlock()
ip := rand.Intn(10)
iface := rand.Intn(100)
sc.Put(now, provider.Query{
ExporterIP: netip.MustParseAddr(fmt.Sprintf("::ffff:127.0.0.%d", ip)),
IfIndex: uint(iface),
}, provider.Answer{
Exporter: provider.Exporter{Name: fmt.Sprintf("localhost%d", ip)},
Interface: provider.Interface{Name: "Gi0/0/0/1", Description: "Transit"},
})
select {
case <-done:
return
default:
}
}
}()
}
var lookups int64
for range 20 {
wg.Add(1)
go func() {
defer wg.Done()
for {
nowLock.RLock()
now := now
nowLock.RUnlock()
ip := rand.Intn(10)
iface := rand.Intn(100)
sc.Lookup(now, provider.Query{
ExporterIP: netip.MustParseAddr(fmt.Sprintf("::ffff:127.0.0.%d", ip)),
IfIndex: uint(iface),
})
for {
current := atomic.LoadInt64(&lookups)
if current == math.MaxInt64 {
return
}
if atomic.CompareAndSwapInt64(&lookups, current, current+1) {
break
}
}
select {
case <-done:
return
default:
}
}
}()
}
time.Sleep(30 * time.Millisecond)
nowLock.RLock()
sc.Expire(now.Add(-5 * time.Minute))
nowLock.RUnlock()
time.Sleep(30 * time.Millisecond)
close(done)
wg.Wait()
for remaining := 20; remaining >= 0; remaining-- {
gotMetrics := r.GetMetrics("akvorado_outlet_metadata_cache_")
hits, err := strconv.ParseFloat(gotMetrics["hits_total"], 64)
if err != nil {
t.Fatalf("strconv.ParseFloat() error:\n%+v", err)
}
misses, err := strconv.ParseFloat(gotMetrics["misses_total"], 64)
if err != nil {
t.Fatalf("strconv.ParseFloat() error:\n%+v", err)
}
got := int64(hits + misses)
expected := atomic.LoadInt64(&lookups)
if got == expected {
break
} else if remaining == 0 {
t.Errorf("hit + miss = %d, expected %d", got, expected)
}
time.Sleep(30 * time.Millisecond)
}
}