proxy-pool/internal/controller/provider/stats.go
2026-08-02 13:55:23 +08:00

150 lines
3.8 KiB
Go

package provider
import (
"errors"
"math"
"reflect"
"sync"
"proxy-pool/internal/domain/upstream"
)
var ErrInvalidStatsRecorder = errors.New("invalid Provider stats recorder")
type Stats struct {
UpstreamID string
ConsecutiveEmptyFetch int64
FetchErrorCount int64
}
type StatsReader interface {
ReadProviderStats([]string) []Stats
}
type StatsRetainer interface {
RetainProviderStats([]string)
}
type StatsRecorder struct {
mu sync.Mutex
maximum int
byID map[string]Stats
observers []ResultObserver
}
func NewStatsRecorder(maximum int) (*StatsRecorder, error) {
if maximum <= 0 {
return nil, ErrInvalidStatsRecorder
}
return &StatsRecorder{maximum: maximum, byID: make(map[string]Stats)}, nil
}
func (recorder *StatsRecorder) Record(result Result) {
if recorder == nil || result.UpstreamID == "" || !validResultClass(result.Class) {
return
}
recorder.mu.Lock()
observers := append([]ResultObserver(nil), recorder.observers...)
stats, exists := recorder.byID[result.UpstreamID]
if !exists {
if len(recorder.byID) >= recorder.maximum {
recorder.mu.Unlock()
recorder.observe(observers, result)
return
}
stats.UpstreamID = result.UpstreamID
}
switch result.Class {
case upstream.FetchEmpty:
if stats.ConsecutiveEmptyFetch < math.MaxInt64 {
stats.ConsecutiveEmptyFetch++
}
case upstream.FetchValid, upstream.FetchDuplicateOnly:
stats.ConsecutiveEmptyFetch = 0
case upstream.FetchError:
if stats.FetchErrorCount < math.MaxInt64 {
stats.FetchErrorCount++
}
}
recorder.byID[result.UpstreamID] = stats
recorder.mu.Unlock()
recorder.observe(observers, result)
}
// AddResultObserver appends one process-local observer. Duplicate instances
// are ignored so repeated bootstrap assembly cannot double-count metrics.
func (recorder *StatsRecorder) AddResultObserver(observer ResultObserver) {
if recorder == nil || observer == nil {
return
}
recorder.mu.Lock()
defer recorder.mu.Unlock()
for _, existing := range recorder.observers {
if sameResultObserver(existing, observer) {
return
}
}
recorder.observers = append(recorder.observers, observer)
}
func (recorder *StatsRecorder) observe(observers []ResultObserver, result Result) {
for _, observer := range observers {
observer.ObserveProviderResult(result)
}
}
func validResultClass(class upstream.FetchClass) bool {
switch class {
case upstream.FetchValid, upstream.FetchEmpty, upstream.FetchDuplicateOnly, upstream.FetchError:
return true
default:
return false
}
}
func sameResultObserver(left, right ResultObserver) bool {
leftValue := reflect.ValueOf(left)
rightValue := reflect.ValueOf(right)
if !leftValue.IsValid() || !rightValue.IsValid() || leftValue.Type() != rightValue.Type() || !leftValue.Type().Comparable() {
return false
}
return leftValue.Interface() == rightValue.Interface()
}
func (recorder *StatsRecorder) ReadProviderStats(upstreamIDs []string) []Stats {
result := make([]Stats, len(upstreamIDs))
if recorder == nil {
return result
}
recorder.mu.Lock()
defer recorder.mu.Unlock()
for index, upstreamID := range upstreamIDs {
result[index] = recorder.byID[upstreamID]
result[index].UpstreamID = upstreamID
}
return result
}
// RetainProviderStats removes observations for upstreams no longer present in
// the complete configuration. Disabled but configured upstreams must be kept.
func (recorder *StatsRecorder) RetainProviderStats(upstreamIDs []string) {
if recorder == nil {
return
}
retained := make(map[string]struct{}, len(upstreamIDs))
for _, upstreamID := range upstreamIDs {
if upstreamID != "" {
retained[upstreamID] = struct{}{}
}
}
recorder.mu.Lock()
defer recorder.mu.Unlock()
for upstreamID := range recorder.byID {
if _, keep := retained[upstreamID]; !keep {
delete(recorder.byID, upstreamID)
}
}
}
var _ ResultObserverRegistrar = (*StatsRecorder)(nil)