123 lines
3.3 KiB
Go
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
|
|
}
|