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)