Files
sing-box-rr-edition/sing-box-rr/common/urltest/networkquality.go
T

123 lines
3.3 KiB
Go

package urltest
import (
"context"
"errors"
"time"
"github.com/sagernet/sing-box/common/networkquality"
)
// NetworkQualityCacheKey identifies equivalent speed measurements within one Box.
type NetworkQualityCacheKey struct {
Server string
Target string
MaxRuntime time.Duration
HTTP3 bool
}
type networkQualityCacheEntry struct {
measuredAt time.Time
result *networkquality.Result
lastTestAt time.Time
lastError error
}
type networkQualityCall struct {
done chan struct{}
entry *networkQualityCacheEntry
err error
}
// LoadOrRunNetworkQuality reuses a recent result or rate limit, or joins an identical in-flight test.
func (s *HistoryStorage) LoadOrRunNetworkQuality(
ctx context.Context,
key NetworkQualityCacheKey,
ttl time.Duration,
run func(context.Context) (*networkquality.Result, error),
) (*networkquality.Result, time.Time, error) {
for {
now := time.Now()
s.access.Lock()
previous := s.networkQualityHistory[key]
if previous != nil && now.Sub(previous.lastTestAt) <= ttl {
s.access.Unlock()
return copyNetworkQualityEntry(previous, previous.lastError)
}
if call := s.networkQualityTestsInProgress[key]; call != nil {
s.access.Unlock()
select {
case <-ctx.Done():
return nil, time.Time{}, ctx.Err()
case <-call.done:
if err := ctx.Err(); err != nil {
return nil, time.Time{}, err
}
if errors.Is(call.err, context.Canceled) || errors.Is(call.err, context.DeadlineExceeded) {
continue
}
return copyNetworkQualityEntry(call.entry, call.err)
}
}
call := &networkQualityCall{
done: make(chan struct{}),
entry: previous,
}
s.networkQualityTestsInProgress[key] = call
s.access.Unlock()
result, err := s.RunNetworkQuality(ctx, run)
measuredAt := time.Now()
if err == nil && result == nil {
err = errors.New("network quality test returned no result")
}
s.access.Lock()
if err == nil {
resultCopy := *result
call.entry = &networkQualityCacheEntry{
measuredAt: measuredAt,
result: &resultCopy,
lastTestAt: measuredAt,
}
s.networkQualityHistory[key] = call.entry
} else if errors.Is(err, networkquality.ErrRateLimited) {
call.entry = &networkQualityCacheEntry{
lastTestAt: measuredAt,
lastError: err,
}
if previous != nil && previous.result != nil {
resultCopy := *previous.result
call.entry.measuredAt = previous.measuredAt
call.entry.result = &resultCopy
}
s.networkQualityHistory[key] = call.entry
}
call.err = err
delete(s.networkQualityTestsInProgress, key)
close(call.done)
s.access.Unlock()
return copyNetworkQualityEntry(call.entry, call.err)
}
}
// RunNetworkQuality executes a measurement through the instance-wide serial slot.
func (s *HistoryStorage) RunNetworkQuality(ctx context.Context, run func(context.Context) (*networkquality.Result, error)) (*networkquality.Result, error) {
select {
case s.networkQualityTestSlot <- struct{}{}:
defer func() { <-s.networkQualityTestSlot }()
return run(ctx)
case <-ctx.Done():
return nil, ctx.Err()
}
}
func copyNetworkQualityEntry(entry *networkQualityCacheEntry, err error) (*networkquality.Result, time.Time, error) {
if entry == nil || entry.result == nil {
return nil, time.Time{}, err
}
result := *entry.result
return &result, entry.measuredAt, err
}