Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions adapter/mock/mock_stats_adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,12 @@ func (a *StatsAdapter) ListStats(patterns ...string) ([]adapter.StatIdentifier,
return statNames, nil
}

// ListSymlinks mocks symlink listing. The mock holds no symlink metadata, so it
// reports none.
func (a *StatsAdapter) ListSymlinks(patterns ...string) ([]adapter.SymlinkEntry, error) {
return nil, nil
}

// DumpStats mocks all stat entries dump.
func (a *StatsAdapter) DumpStats(patterns ...string) ([]adapter.StatEntry, error) {
return a.entries, nil
Expand Down
26 changes: 26 additions & 0 deletions adapter/stats_api.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2019 Cisco and/or its affiliates.
// Copyright (c) 2026 Meter, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -41,6 +42,9 @@ type StatsAPI interface {

// ListStats lists indexed names for stats matching patterns.
ListStats(patterns ...string) (indexes []StatIdentifier, err error)
// ListSymlinks lists symlink entries matching patterns, resolved to the
// entry and item each of them aliases.
ListSymlinks(patterns ...string) (symlinks []SymlinkEntry, err error)
// DumpStats dumps all stat entries.
DumpStats(patterns ...string) (entries []StatEntry, err error)

Expand Down Expand Up @@ -91,6 +95,28 @@ type StatEntry struct {
Symlink bool
}

// SymlinkEntry describes a symlink directory entry and the counter it aliases.
//
// VPP exposes some counters only as one vector plus a set of symlinks naming its
// items: /node/errors is a single counter vector, and every /err/<node>/<reason>
// is a symlink to one item of it. Reading the vector once is far cheaper than
// resolving thousands of symlinks, but then the item names are recoverable only
// from the symlinks, and an item's position is not derivable from anything else
// (vlib_register_errors allocates it from a heap, and reuses freed holes).
//
// ListSymlinks reports that mapping, so a caller reading a vector directly can
// label its items.
type SymlinkEntry struct {
// StatIdentifier holds the name and directory index of the symlink itself.
StatIdentifier
// TargetIndex is the directory index of the aliased entry, in the same index
// space as StatIdentifier.Index, and TargetName is its name.
TargetIndex uint32
TargetName []byte
// ItemIndex is the position within the aliased vector that the symlink names.
ItemIndex uint32
}

// Counter represents simple counter with single value, which is usually packet count.
type Counter uint64

Expand Down
7 changes: 7 additions & 0 deletions adapter/statsclient/stat_segment_api.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2020 Cisco and/or its affiliates.
// Copyright (c) 2026 Meter, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -93,6 +94,12 @@ type statSegment interface {
// Use ^uint32(0) as an empty index (since 0 is a valid value).
CopyEntryData(segment dirSegment, index uint32) adapter.Stat

// GetSymlinkIndexes returns, for a symlink directory segment, the directory
// index of the entry it aliases and the item index within that entry.
// ok is false if the segment is not a symlink, or if the segment version has
// no notion of symlinks.
GetSymlinkIndexes(segment dirSegment) (targetIndex, itemIndex uint32, ok bool)

// UpdateEntryData accepts pointer to a directory segment with data, and stat
// segment to update
UpdateEntryData(segment dirSegment, s *adapter.Stat) error
Expand Down
116 changes: 106 additions & 10 deletions adapter/statsclient/statsclient.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2019 Cisco and/or its affiliates.
// Copyright (c) 2026 Meter, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -186,6 +187,36 @@ func (sc *StatsClient) ListStats(patterns ...string) (entries []adapter.StatIden
return entries, nil
}

// ListSymlinks lists the symlinks among the entries matching patterns, resolved to
// the entry and item each aliases. Entries that are not symlinks are skipped, so
// patterns may be as broad as convenient.
//
// Like ListStats it reads names and indexes only - no counter data is copied - which
// makes it cheap enough to rebuild the mapping on every epoch change. See
// adapter.SymlinkEntry for what the mapping is good for.
func (sc *StatsClient) ListSymlinks(patterns ...string) (symlinks []adapter.SymlinkEntry, err error) {
sc.accessLock.RLock()
defer sc.accessLock.RUnlock()

if !sc.isConnected() {
return nil, adapter.ErrStatsDisconnected
}
accessEpoch := sc.accessStart()
if accessEpoch == 0 {
return nil, adapter.ErrStatsAccessFailed
}

symlinks, err = sc.getSymlinkEntries(patterns...)
if err != nil {
return nil, err
}

if !sc.accessEnd(accessEpoch) {
return nil, adapter.ErrStatsDataBusy
}
return symlinks, nil
}

func (sc *StatsClient) DumpStats(patterns ...string) (entries []adapter.StatEntry, err error) {
sc.accessLock.RLock()
defer sc.accessLock.RUnlock()
Expand Down Expand Up @@ -282,18 +313,21 @@ func (sc *StatsClient) UpdateDir(dir *adapter.StatDir) (err error) {
return adapter.ErrStatsDisconnected
}

epoch, _ := sc.GetEpoch()
if dir.Epoch != epoch {
return adapter.ErrStatsDirStale
}

// Compare the prepared dir against the epoch accessStart settled on, not against
// a separately read one: with two reads the directory can be re-laid-out in
// between, in which case the staleness check passes against the old epoch while
// the entries are resolved against the new directory - and accessEnd then
// confirms that same new epoch, so nothing catches it.
accessEpoch := sc.accessStart()
if accessEpoch == 0 {
return adapter.ErrStatsAccessFailed
}
if dir.Epoch != accessEpoch {
return adapter.ErrStatsDirStale
}
dirVector := sc.GetDirectoryVector()
if dirVector == nil {
return err
return fmt.Errorf("failed to update dir: directory vector is nil")
}
for i := 0; i < len(dir.Entries); i++ {
if err := sc.updateStatOnIndex(&dir.Entries[i], dirVector); err != nil {
Expand Down Expand Up @@ -572,6 +606,54 @@ func (sc *StatsClient) getIdentifierEntriesOnIndex(vector dirVector, indexes ...
return identifiers, nil
}

// getSymlinkEntries retrieves the symlinks among the entries matching desired
// patterns, or among all entries if no pattern is provided.
func (sc *StatsClient) getSymlinkEntries(patterns ...string) (symlinks []adapter.SymlinkEntry, err error) {
vector := sc.GetDirectoryVector()
if vector == nil {
return nil, fmt.Errorf("failed to get symlink entries: directory vector is nil")
}
indexes, err := sc.listIndexes(vector, patterns...)
if err != nil {
return nil, err
}
return sc.getSymlinkEntriesOnIndex(vector, indexes...)
}

// getSymlinkEntriesOnIndex resolves the symlinks among indexes to the entry and item
// each of them aliases. Indexes that are not symlinks are skipped.
func (sc *StatsClient) getSymlinkEntriesOnIndex(vector dirVector, indexes ...uint32) (symlinks []adapter.SymlinkEntry, err error) {
dirLen := *(*uint32)(vectorLen(vector))
for _, index := range indexes {
if index >= dirLen {
return nil, fmt.Errorf("stat entry index %d out of dir vector length (%d)", index, dirLen)
}
dirPtr, dirName, _ := sc.GetStatDirOnIndex(vector, index)
if len(dirName) == 0 {
continue
}
targetIndex, itemIndex, ok := sc.GetSymlinkIndexes(dirPtr)
if !ok {
continue
}
if targetIndex >= dirLen {
debugf("symlink %s aliases out of range index %d", dirName, targetIndex)
continue
}
_, targetName, _ := sc.GetStatDirOnIndex(vector, targetIndex)
symlinks = append(symlinks, adapter.SymlinkEntry{
StatIdentifier: adapter.StatIdentifier{
Index: index,
Name: dirName,
},
TargetIndex: targetIndex,
TargetName: targetName,
ItemIndex: itemIndex,
})
}
return symlinks, nil
}

// listIndexes lists indexes for all stat entries that match any of the regex patterns.
func (sc *StatsClient) listIndexes(vector dirVector, patterns ...string) (indexes []uint32, err error) {
if len(patterns) == 0 {
Expand Down Expand Up @@ -629,10 +711,24 @@ func (sc *StatsClient) updateStatOnIndex(entry *adapter.StatEntry, vector dirVec
return fmt.Errorf("stat entry index %d out of dir vector length (%d)", entry.Index, dirLen)
}
dirPtr, dirName, dirType := sc.GetStatDirOnIndex(vector, entry.Index)
if len(dirName) == 0 ||
!bytes.Equal(dirName, entry.Name) ||
dirType != entry.Type ||
entry.Data == nil {
// Identity is the name; if it no longer matches, the directory changed under us
// (the epoch check in UpdateDir normally catches this first).
if len(dirName) == 0 || !bytes.Equal(dirName, entry.Name) || entry.Data == nil {
return nil
}
if dirType == adapter.Symlink {
// A symlink's directory entry holds (target, item) indexes rather than a data
// pointer, so its resolved Type never equals dirType and the type check below
// would skip it, leaving the entry frozen at its PrepareDir value forever.
// Re-resolve through the symlink instead. This allocates, unlike the in-place
// UpdateEntryData path, because the resolved item does not have a stable
// backing slice to write into; callers refreshing large numbers of symlinks
// on a tick are better served reading the backing vector and mapping it with
// ListSymlinks.
entry.Data = sc.CopyEntryData(dirPtr, ^uint32(0))
return nil
}
if dirType != entry.Type {
return nil
}
if err := sc.UpdateEntryData(dirPtr, &entry.Data); err != nil {
Expand Down
7 changes: 7 additions & 0 deletions adapter/statsclient/statseg_v1.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2019 Cisco and/or its affiliates.
// Copyright (c) 2026 Meter, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -92,6 +93,12 @@ func (ss *statSegmentV1) GetEpoch() (int64, bool) {
return sh.epoch, sh.inProgress != 0
}

// GetSymlinkIndexes is unsupported for stats segment v1, which does not encode
// symlink target indexes.
func (ss *statSegmentV1) GetSymlinkIndexes(dirSegment) (uint32, uint32, bool) {
return 0, 0, false
}

func (ss *statSegmentV1) CopyEntryData(segment dirSegment, _ uint32) adapter.Stat {
dirEntry := (*statSegDirectoryEntryV1)(segment)
typ := getStatType(dirEntry.directoryType, true)
Expand Down
12 changes: 12 additions & 0 deletions adapter/statsclient/statseg_v2.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
// Copyright (c) 2020 Cisco and/or its affiliates.
// Copyright (c) 2026 Meter, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -580,6 +581,17 @@ func (ss *statSegmentV2) getErrorVector() dirVector {
return ss.adjust(dirVector(&header.errorVector))
}

// GetSymlinkIndexes returns the target directory index and item index encoded in a
// symlink directory segment's union data, or ok false if the segment is not a symlink.
func (ss *statSegmentV2) GetSymlinkIndexes(segment dirSegment) (targetIndex, itemIndex uint32, ok bool) {
dirEntry := (*statSegDirectoryEntryV2)(segment)
if getStatType(dirEntry.directoryType, ss.getErrorVector() != nil) != adapter.Symlink {
return 0, 0, false
}
targetIndex, itemIndex = ss.getSymlinkIndexes(dirEntry)
return targetIndex, itemIndex, true
}

func (ss *statSegmentV2) getSymlinkIndexes(dirEntry *statSegDirectoryEntryV2) (index1, index2 uint32) {
var b bytes.Buffer
if err := binary.Write(&b, binary.LittleEndian, dirEntry.unionData); err != nil {
Expand Down
Loading
Loading