Feature: add lazy for proxy group and provider

This commit is contained in:
gVisor bot 2020-11-19 00:53:22 +08:00
parent ec41d4f5f7
commit 9c80233034
11 changed files with 97 additions and 59 deletions

View file

@ -11,10 +11,14 @@ const (
defaultGetProxiesDuration = time.Second * 5 defaultGetProxiesDuration = time.Second * 5
) )
func getProvidersProxies(providers []provider.ProxyProvider) []C.Proxy { func getProvidersProxies(providers []provider.ProxyProvider, touch bool) []C.Proxy {
proxies := []C.Proxy{} proxies := []C.Proxy{}
for _, provider := range providers { for _, provider := range providers {
proxies = append(proxies, provider.Proxies()...) if touch {
proxies = append(proxies, provider.ProxiesWithTouch()...)
} else {
proxies = append(proxies, provider.Proxies()...)
}
} }
return proxies return proxies
} }

View file

@ -18,12 +18,12 @@ type Fallback struct {
} }
func (f *Fallback) Now() string { func (f *Fallback) Now() string {
proxy := f.findAliveProxy() proxy := f.findAliveProxy(false)
return proxy.Name() return proxy.Name()
} }
func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
proxy := f.findAliveProxy() proxy := f.findAliveProxy(true)
c, err := proxy.DialContext(ctx, metadata) c, err := proxy.DialContext(ctx, metadata)
if err == nil { if err == nil {
c.AppendToChains(f) c.AppendToChains(f)
@ -32,7 +32,7 @@ func (f *Fallback) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con
} }
func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (f *Fallback) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
proxy := f.findAliveProxy() proxy := f.findAliveProxy(true)
pc, err := proxy.DialUDP(metadata) pc, err := proxy.DialUDP(metadata)
if err == nil { if err == nil {
pc.AppendToChains(f) pc.AppendToChains(f)
@ -45,13 +45,13 @@ func (f *Fallback) SupportUDP() bool {
return false return false
} }
proxy := f.findAliveProxy() proxy := f.findAliveProxy(false)
return proxy.SupportUDP() return proxy.SupportUDP()
} }
func (f *Fallback) MarshalJSON() ([]byte, error) { func (f *Fallback) MarshalJSON() ([]byte, error) {
var all []string var all []string
for _, proxy := range f.proxies() { for _, proxy := range f.proxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]interface{}{
@ -62,27 +62,27 @@ func (f *Fallback) MarshalJSON() ([]byte, error) {
} }
func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy { func (f *Fallback) Unwrap(metadata *C.Metadata) C.Proxy {
proxy := f.findAliveProxy() proxy := f.findAliveProxy(true)
return proxy return proxy
} }
func (f *Fallback) proxies() []C.Proxy { func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() (interface{}, error) { elm, _, _ := f.single.Do(func() (interface{}, error) {
return getProvidersProxies(f.providers), nil return getProvidersProxies(f.providers, touch), nil
}) })
return elm.([]C.Proxy) return elm.([]C.Proxy)
} }
func (f *Fallback) findAliveProxy() C.Proxy { func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.proxies() proxies := f.proxies(touch)
for _, proxy := range proxies { for _, proxy := range proxies {
if proxy.Alive() { if proxy.Alive() {
return proxy return proxy
} }
} }
return f.proxies()[0] return proxies[0]
} }
func NewFallback(options *GroupCommonOption, providers []provider.ProxyProvider) *Fallback { func NewFallback(options *GroupCommonOption, providers []provider.ProxyProvider) *Fallback {

View file

@ -131,13 +131,13 @@ func strategyConsistentHashing() strategyFn {
} }
func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy { func (lb *LoadBalance) Unwrap(metadata *C.Metadata) C.Proxy {
proxies := lb.proxies() proxies := lb.proxies(true)
return lb.strategyFn(proxies, metadata) return lb.strategyFn(proxies, metadata)
} }
func (lb *LoadBalance) proxies() []C.Proxy { func (lb *LoadBalance) proxies(touch bool) []C.Proxy {
elm, _, _ := lb.single.Do(func() (interface{}, error) { elm, _, _ := lb.single.Do(func() (interface{}, error) {
return getProvidersProxies(lb.providers), nil return getProvidersProxies(lb.providers, touch), nil
}) })
return elm.([]C.Proxy) return elm.([]C.Proxy)
@ -145,7 +145,7 @@ func (lb *LoadBalance) proxies() []C.Proxy {
func (lb *LoadBalance) MarshalJSON() ([]byte, error) { func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
var all []string var all []string
for _, proxy := range lb.proxies() { for _, proxy := range lb.proxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]interface{}{

View file

@ -24,13 +24,16 @@ type GroupCommonOption struct {
Use []string `group:"use,omitempty"` Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"` URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"` Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,omitempty"`
DisableUDP bool `group:"disable-udp,omitempty"` DisableUDP bool `group:"disable-udp,omitempty"`
} }
func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) { func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy, providersMap map[string]provider.ProxyProvider) (C.ProxyAdapter, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
groupOption := &GroupCommonOption{} groupOption := &GroupCommonOption{
Lazy: true,
}
if err := decoder.Decode(config, groupOption); err != nil { if err := decoder.Decode(config, groupOption); err != nil {
return nil, errFormat return nil, errFormat
} }
@ -55,7 +58,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
// if Use not empty, drop health check options // if Use not empty, drop health check options
if len(groupOption.Use) != 0 { if len(groupOption.Use) != 0 {
hc := provider.NewHealthCheck(ps, "", 0) hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc) pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil { if err != nil {
return nil, err return nil, err
@ -69,7 +72,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
// select don't need health check // select don't need health check
if groupOption.Type == "select" || groupOption.Type == "relay" { if groupOption.Type == "select" || groupOption.Type == "relay" {
hc := provider.NewHealthCheck(ps, "", 0) hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc) pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil { if err != nil {
return nil, err return nil, err
@ -82,7 +85,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
return nil, errMissHealthCheck return nil, errMissHealthCheck
} }
hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval)) hc := provider.NewHealthCheck(ps, groupOption.URL, uint(groupOption.Interval), groupOption.Lazy)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc) pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -20,7 +20,7 @@ type Relay struct {
} }
func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
proxies := r.proxies(metadata) proxies := r.proxies(metadata, true)
if len(proxies) == 0 { if len(proxies) == 0 {
return nil, errors.New("proxy does not exist") return nil, errors.New("proxy does not exist")
} }
@ -58,7 +58,7 @@ func (r *Relay) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn,
func (r *Relay) MarshalJSON() ([]byte, error) { func (r *Relay) MarshalJSON() ([]byte, error) {
var all []string var all []string
for _, proxy := range r.rawProxies() { for _, proxy := range r.rawProxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]interface{}{
@ -67,16 +67,16 @@ func (r *Relay) MarshalJSON() ([]byte, error) {
}) })
} }
func (r *Relay) rawProxies() []C.Proxy { func (r *Relay) rawProxies(touch bool) []C.Proxy {
elm, _, _ := r.single.Do(func() (interface{}, error) { elm, _, _ := r.single.Do(func() (interface{}, error) {
return getProvidersProxies(r.providers), nil return getProvidersProxies(r.providers, touch), nil
}) })
return elm.([]C.Proxy) return elm.([]C.Proxy)
} }
func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy { func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
proxies := r.rawProxies() proxies := r.rawProxies(touch)
for n, proxy := range proxies { for n, proxy := range proxies {
subproxy := proxy.Unwrap(metadata) subproxy := proxy.Unwrap(metadata)

View file

@ -20,7 +20,7 @@ type Selector struct {
} }
func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) { func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Conn, error) {
c, err := s.selectedProxy().DialContext(ctx, metadata) c, err := s.selectedProxy(true).DialContext(ctx, metadata)
if err == nil { if err == nil {
c.AppendToChains(s) c.AppendToChains(s)
} }
@ -28,7 +28,7 @@ func (s *Selector) DialContext(ctx context.Context, metadata *C.Metadata) (C.Con
} }
func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (s *Selector) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := s.selectedProxy().DialUDP(metadata) pc, err := s.selectedProxy(true).DialUDP(metadata)
if err == nil { if err == nil {
pc.AppendToChains(s) pc.AppendToChains(s)
} }
@ -40,12 +40,12 @@ func (s *Selector) SupportUDP() bool {
return false return false
} }
return s.selectedProxy().SupportUDP() return s.selectedProxy(false).SupportUDP()
} }
func (s *Selector) MarshalJSON() ([]byte, error) { func (s *Selector) MarshalJSON() ([]byte, error) {
var all []string var all []string
for _, proxy := range getProvidersProxies(s.providers) { for _, proxy := range getProvidersProxies(s.providers, false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
@ -57,11 +57,11 @@ func (s *Selector) MarshalJSON() ([]byte, error) {
} }
func (s *Selector) Now() string { func (s *Selector) Now() string {
return s.selectedProxy().Name() return s.selectedProxy(false).Name()
} }
func (s *Selector) Set(name string) error { func (s *Selector) Set(name string) error {
for _, proxy := range getProvidersProxies(s.providers) { for _, proxy := range getProvidersProxies(s.providers, false) {
if proxy.Name() == name { if proxy.Name() == name {
s.selected = name s.selected = name
s.single.Reset() s.single.Reset()
@ -73,12 +73,12 @@ func (s *Selector) Set(name string) error {
} }
func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy { func (s *Selector) Unwrap(metadata *C.Metadata) C.Proxy {
return s.selectedProxy() return s.selectedProxy(true)
} }
func (s *Selector) selectedProxy() C.Proxy { func (s *Selector) selectedProxy(touch bool) C.Proxy {
elm, _, _ := s.single.Do(func() (interface{}, error) { elm, _, _ := s.single.Do(func() (interface{}, error) {
proxies := getProvidersProxies(s.providers) proxies := getProvidersProxies(s.providers, touch)
for _, proxy := range proxies { for _, proxy := range proxies {
if proxy.Name() == s.selected { if proxy.Name() == s.selected {
return proxy, nil return proxy, nil

View file

@ -30,11 +30,11 @@ type URLTest struct {
} }
func (u *URLTest) Now() string { func (u *URLTest) Now() string {
return u.fast().Name() return u.fast(false).Name()
} }
func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) { func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Conn, err error) {
c, err = u.fast().DialContext(ctx, metadata) c, err = u.fast(true).DialContext(ctx, metadata)
if err == nil { if err == nil {
c.AppendToChains(u) c.AppendToChains(u)
} }
@ -42,7 +42,7 @@ func (u *URLTest) DialContext(ctx context.Context, metadata *C.Metadata) (c C.Co
} }
func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) { func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
pc, err := u.fast().DialUDP(metadata) pc, err := u.fast(true).DialUDP(metadata)
if err == nil { if err == nil {
pc.AppendToChains(u) pc.AppendToChains(u)
} }
@ -50,20 +50,20 @@ func (u *URLTest) DialUDP(metadata *C.Metadata) (C.PacketConn, error) {
} }
func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy { func (u *URLTest) Unwrap(metadata *C.Metadata) C.Proxy {
return u.fast() return u.fast(true)
} }
func (u *URLTest) proxies() []C.Proxy { func (u *URLTest) proxies(touch bool) []C.Proxy {
elm, _, _ := u.single.Do(func() (interface{}, error) { elm, _, _ := u.single.Do(func() (interface{}, error) {
return getProvidersProxies(u.providers), nil return getProvidersProxies(u.providers, touch), nil
}) })
return elm.([]C.Proxy) return elm.([]C.Proxy)
} }
func (u *URLTest) fast() C.Proxy { func (u *URLTest) fast(touch bool) C.Proxy {
elm, _, _ := u.fastSingle.Do(func() (interface{}, error) { elm, _, _ := u.fastSingle.Do(func() (interface{}, error) {
proxies := u.proxies() proxies := u.proxies(touch)
fast := proxies[0] fast := proxies[0]
min := fast.LastDelay() min := fast.LastDelay()
for _, proxy := range proxies[1:] { for _, proxy := range proxies[1:] {
@ -94,12 +94,12 @@ func (u *URLTest) SupportUDP() bool {
return false return false
} }
return u.fast().SupportUDP() return u.fast(false).SupportUDP()
} }
func (u *URLTest) MarshalJSON() ([]byte, error) { func (u *URLTest) MarshalJSON() ([]byte, error) {
var all []string var all []string
for _, proxy := range u.proxies() { for _, proxy := range u.proxies(false) {
all = append(all, proxy.Name()) all = append(all, proxy.Name())
} }
return json.Marshal(map[string]interface{}{ return json.Marshal(map[string]interface{}{

View file

@ -5,6 +5,8 @@ import (
"time" "time"
C "github.com/Dreamacro/clash/constant" C "github.com/Dreamacro/clash/constant"
"go.uber.org/atomic"
) )
const ( const (
@ -17,10 +19,12 @@ type HealthCheckOption struct {
} }
type HealthCheck struct { type HealthCheck struct {
url string url string
proxies []C.Proxy proxies []C.Proxy
interval uint interval uint
done chan struct{} lazy bool
lastTouch *atomic.Int64
done chan struct{}
} }
func (hc *HealthCheck) process() { func (hc *HealthCheck) process() {
@ -30,7 +34,10 @@ func (hc *HealthCheck) process() {
for { for {
select { select {
case <-ticker.C: case <-ticker.C:
hc.check() now := time.Now().Unix()
if !hc.lazy || now-hc.lastTouch.Load() < int64(hc.interval) {
hc.check()
}
case <-hc.done: case <-hc.done:
ticker.Stop() ticker.Stop()
return return
@ -46,6 +53,10 @@ func (hc *HealthCheck) auto() bool {
return hc.interval != 0 return hc.interval != 0
} }
func (hc *HealthCheck) touch() {
hc.lastTouch.Store(time.Now().Unix())
}
func (hc *HealthCheck) check() { func (hc *HealthCheck) check() {
ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout) ctx, cancel := context.WithTimeout(context.Background(), defaultURLTestTimeout)
for _, proxy := range hc.proxies { for _, proxy := range hc.proxies {
@ -60,11 +71,13 @@ func (hc *HealthCheck) close() {
hc.done <- struct{}{} hc.done <- struct{}{}
} }
func NewHealthCheck(proxies []C.Proxy, url string, interval uint) *HealthCheck { func NewHealthCheck(proxies []C.Proxy, url string, interval uint, lazy bool) *HealthCheck {
return &HealthCheck{ return &HealthCheck{
proxies: proxies, proxies: proxies,
url: url, url: url,
interval: interval, interval: interval,
done: make(chan struct{}, 1), lazy: lazy,
lastTouch: atomic.NewInt64(0),
done: make(chan struct{}, 1),
} }
} }

View file

@ -17,6 +17,7 @@ type healthCheckSchema struct {
Enable bool `provider:"enable"` Enable bool `provider:"enable"`
URL string `provider:"url"` URL string `provider:"url"`
Interval int `provider:"interval"` Interval int `provider:"interval"`
Lazy bool `provider:"lazy,omitempty"`
} }
type proxyProviderSchema struct { type proxyProviderSchema struct {
@ -30,7 +31,11 @@ type proxyProviderSchema struct {
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) { func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) {
decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true}) decoder := structure.NewDecoder(structure.Option{TagName: "provider", WeaklyTypedInput: true})
schema := &proxyProviderSchema{} schema := &proxyProviderSchema{
HealthCheck: healthCheckSchema{
Lazy: true,
},
}
if err := decoder.Decode(mapping, schema); err != nil { if err := decoder.Decode(mapping, schema); err != nil {
return nil, err return nil, err
} }
@ -39,7 +44,7 @@ func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvi
if schema.HealthCheck.Enable { if schema.HealthCheck.Enable {
hcInterval = uint(schema.HealthCheck.Interval) hcInterval = uint(schema.HealthCheck.Interval)
} }
hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval) hc := NewHealthCheck([]C.Proxy{}, schema.HealthCheck.URL, hcInterval, schema.HealthCheck.Lazy)
path := C.Path.Resolve(schema.Path) path := C.Path.Resolve(schema.Path)

View file

@ -50,6 +50,9 @@ type Provider interface {
type ProxyProvider interface { type ProxyProvider interface {
Provider Provider
Proxies() []C.Proxy Proxies() []C.Proxy
// ProxiesWithTouch is used to inform the provider that the proxy is actually being used while getting the list of proxies.
// Commonly used in Dial and DialUDP
ProxiesWithTouch() []C.Proxy
HealthCheck() HealthCheck()
} }
@ -112,6 +115,11 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies return pp.proxies
} }
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
pp.healthCheck.touch()
return pp.Proxies()
}
func proxiesParse(buf []byte) (interface{}, error) { func proxiesParse(buf []byte) (interface{}, error) {
schema := &ProxySchema{} schema := &ProxySchema{}
@ -223,6 +231,11 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies return cp.proxies
} }
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
cp.healthCheck.touch()
return cp.Proxies()
}
func stopCompatibleProvider(pd *CompatibleProvider) { func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close() pd.healthCheck.close()
} }

View file

@ -345,7 +345,7 @@ func parseProxies(cfg *RawConfig) (proxies map[string]C.Proxy, providersMap map[
for _, v := range proxyList { for _, v := range proxyList {
ps = append(ps, proxies[v]) ps = append(ps, proxies[v])
} }
hc := provider.NewHealthCheck(ps, "", 0) hc := provider.NewHealthCheck(ps, "", 0, true)
pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc) pd, _ := provider.NewCompatibleProvider(provider.ReservedName, ps, hc)
providersMap[provider.ReservedName] = pd providersMap[provider.ReservedName] = pd