Files
akvorado/inlet/snmp/cache.go
Vincent Bernat 78fb01c223 chore: fix some small issues detected by golangci-lint
But not using it as some linters are either plain incorrect (the one
suggesting to not use nil for `c.t.Context()`) or just
debatable (checking for err value is a good practice, but there are
good reasons to opt out in some cases).
2022-08-10 17:44:32 +02:00

296 lines
7.7 KiB
Go

// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only
package snmp
import (
"bytes"
"encoding/gob"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
"sync/atomic"
"time"
"github.com/benbjohnson/clock"
"akvorado/common/reporter"
)
var (
// ErrCacheMiss is triggered on lookup cache miss
ErrCacheMiss = errors.New("SNMP cache miss")
// ErrCacheVersion is triggered when loading a cache from an incompatible version
ErrCacheVersion = errors.New("SNMP cache version mismatch")
// cacheCurrentVersionNumber is the current version of the on-disk cache format
cacheCurrentVersionNumber = 8
)
// snmpCache represents the SNMP cache.
type snmpCache struct {
r *reporter.Reporter
cache map[string]*cachedExporter
cacheLock sync.RWMutex
clock clock.Clock
metrics struct {
cacheHit reporter.Counter
cacheMiss reporter.Counter
cacheExpired reporter.Counter
cacheSize reporter.GaugeFunc
cacheExporters reporter.GaugeFunc
}
}
// cachedExporter represents information about a exporter. It includes
// the mapping from ifIndex to interfaces.
type cachedExporter struct {
Name string
Interfaces map[uint]*cachedInterface
}
// Interface contains the information about an interface.
type Interface struct {
Name string
Description string
Speed uint
}
// cachedInterface contains the information about a cached interface.
type cachedInterface struct {
LastUpdated int64
LastAccessed int64
Interface
}
func newSNMPCache(r *reporter.Reporter, clock clock.Clock) *snmpCache {
sc := &snmpCache{
r: r,
cache: make(map[string]*cachedExporter),
clock: clock,
}
sc.metrics.cacheHit = r.Counter(
reporter.CounterOpts{
Name: "cache_hit",
Help: "Number of lookups retrieved from cache.",
})
sc.metrics.cacheMiss = r.Counter(
reporter.CounterOpts{
Name: "cache_miss",
Help: "Number of lookup miss.",
})
sc.metrics.cacheExpired = r.Counter(
reporter.CounterOpts{
Name: "cache_expired",
Help: "Number of cache entries expired.",
})
sc.metrics.cacheSize = r.GaugeFunc(
reporter.GaugeOpts{
Name: "cache_size",
Help: "Number of entries in cache.",
}, func() (result float64) {
sc.cacheLock.RLock()
defer sc.cacheLock.RUnlock()
for _, exporter := range sc.cache {
result += float64(len(exporter.Interfaces))
}
return
})
sc.metrics.cacheExporters = r.GaugeFunc(
reporter.GaugeOpts{
Name: "cache_exporters",
Help: "Number of exporters in cache.",
}, func() float64 {
sc.cacheLock.RLock()
defer sc.cacheLock.RUnlock()
return float64(len(sc.cache))
})
return sc
}
// Lookup will perform a lookup of the cache. It returns the exporter
// name as well as the requested interface.
func (sc *snmpCache) Lookup(ip string, ifIndex uint) (string, Interface, error) {
return sc.lookup(ip, ifIndex, true)
}
func (sc *snmpCache) lookup(ip string, ifIndex uint, touchAccess bool) (string, Interface, error) {
sc.cacheLock.RLock()
defer sc.cacheLock.RUnlock()
exporter, ok := sc.cache[ip]
if !ok {
sc.metrics.cacheMiss.Inc()
return "", Interface{}, ErrCacheMiss
}
iface, ok := exporter.Interfaces[ifIndex]
if !ok {
sc.metrics.cacheMiss.Inc()
return "", Interface{}, ErrCacheMiss
}
sc.metrics.cacheHit.Inc()
if touchAccess {
atomic.StoreInt64(&iface.LastAccessed, sc.clock.Now().Unix())
}
return exporter.Name, iface.Interface, nil
}
// Put a new entry in the cache.
func (sc *snmpCache) Put(ip string, exporterName string, ifIndex uint, iface Interface) {
sc.cacheLock.Lock()
defer sc.cacheLock.Unlock()
now := sc.clock.Now().Unix()
ciface := cachedInterface{
LastUpdated: now,
LastAccessed: now,
Interface: iface,
}
exporter, ok := sc.cache[ip]
if !ok {
exporter = &cachedExporter{Interfaces: make(map[uint]*cachedInterface)}
sc.cache[ip] = exporter
}
exporter.Name = exporterName
exporter.Interfaces[ifIndex] = &ciface
}
// Expire expire entries older than the provided duration (rely on last access).
func (sc *snmpCache) Expire(older time.Duration) (count uint) {
threshold := sc.clock.Now().Add(-older).Unix()
sc.cacheLock.Lock()
defer sc.cacheLock.Unlock()
for ip, exporter := range sc.cache {
for ifindex, iface := range exporter.Interfaces {
if iface.LastAccessed < threshold {
delete(exporter.Interfaces, ifindex)
sc.metrics.cacheExpired.Inc()
count++
}
}
if len(exporter.Interfaces) == 0 {
delete(sc.cache, ip)
}
}
return
}
// Return entries older than the provided duration. If LastAccessed is
// true, rely on last access, otherwise on last update.
func (sc *snmpCache) entriesOlderThan(older time.Duration, lastAccessed bool) map[string]map[uint]Interface {
threshold := sc.clock.Now().Add(-older).Unix()
result := make(map[string]map[uint]Interface)
sc.cacheLock.RLock()
defer sc.cacheLock.RUnlock()
for ip, exporter := range sc.cache {
for ifindex, iface := range exporter.Interfaces {
what := &iface.LastAccessed
if !lastAccessed {
what = &iface.LastUpdated
}
if atomic.LoadInt64(what) < threshold {
_, ok := result[ip]
if !ok {
rifaces := make(map[uint]Interface)
result[ip] = rifaces
}
result[ip][ifindex] = iface.Interface
}
}
}
return result
}
// Need updates returns a map of interface entries that would need to
// be updated. It relies on last update.
func (sc *snmpCache) NeedUpdates(older time.Duration) map[string]map[uint]Interface {
return sc.entriesOlderThan(older, false)
}
// Need updates returns a map of interface entries that would have
// expired. It relies on last access.
func (sc *snmpCache) WouldExpire(older time.Duration) map[string]map[uint]Interface {
return sc.entriesOlderThan(older, true)
}
// Save stores the cache to the provided location.
func (sc *snmpCache) Save(cacheFile string) error {
tmpFile, err := ioutil.TempFile(
filepath.Dir(cacheFile),
fmt.Sprintf("%s-*", filepath.Base(cacheFile)))
if err != nil {
return fmt.Errorf("unable to create cache file %q: %w", cacheFile, err)
}
defer func() {
tmpFile.Close() // ignore errors
os.Remove(tmpFile.Name()) // ignore errors
}()
// Write cache
encoder := gob.NewEncoder(tmpFile)
if err := encoder.Encode(sc); err != nil {
return fmt.Errorf("unable to encode cache: %w", err)
}
// Move cache to new location
if err := os.Rename(tmpFile.Name(), cacheFile); err != nil {
return fmt.Errorf("unable to write cache file %q: %w", cacheFile, err)
}
return nil
}
// Load loads the cache from the provided location.
func (sc *snmpCache) Load(cacheFile string) error {
f, err := os.Open(cacheFile)
if err != nil {
return fmt.Errorf("unable to load cache %q: %w", cacheFile, err)
}
decoder := gob.NewDecoder(f)
if err := decoder.Decode(sc); err != nil {
return fmt.Errorf("unable to decode cache: %w", err)
}
return nil
}
// GobEncode encodes the SNMP cache.
func (sc *snmpCache) GobEncode() ([]byte, error) {
var buf bytes.Buffer
encoder := gob.NewEncoder(&buf)
if err := encoder.Encode(&cacheCurrentVersionNumber); err != nil {
return nil, err
}
sc.cacheLock.Lock() // needed because we won't do atomic access
defer sc.cacheLock.Unlock()
if err := encoder.Encode(sc.cache); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// GobDecode decodes the SNMP cache.
func (sc *snmpCache) GobDecode(data []byte) error {
buf := bytes.NewBuffer(data)
decoder := gob.NewDecoder(buf)
version := cacheCurrentVersionNumber
if err := decoder.Decode(&version); err != nil {
return err
}
if version != cacheCurrentVersionNumber {
return ErrCacheVersion
}
cache := map[string]*cachedExporter{}
if err := decoder.Decode(&cache); err != nil {
return err
}
sc.cacheLock.Lock()
sc.cache = cache
sc.cacheLock.Unlock()
return nil
}