Feature: add `lazy` for proxy group and provider

This commit is contained in:
Dreamacro 2020-11-19 00:53:22 +08:00
parent 4735f61fd1
commit 0402878daa
11 changed files with 97 additions and 59 deletions

View File

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

View File

@ -18,12 +18,12 @@ type Fallback struct {
}
func (f *Fallback) Now() string {
proxy := f.findAliveProxy()
proxy := f.findAliveProxy(false)
return proxy.Name()
}
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)
if err == nil {
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) {
proxy := f.findAliveProxy()
proxy := f.findAliveProxy(true)
pc, err := proxy.DialUDP(metadata)
if err == nil {
pc.AppendToChains(f)
@ -45,13 +45,13 @@ func (f *Fallback) SupportUDP() bool {
return false
}
proxy := f.findAliveProxy()
proxy := f.findAliveProxy(false)
return proxy.SupportUDP()
}
func (f *Fallback) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range f.proxies() {
for _, proxy := range f.proxies(false) {
all = append(all, proxy.Name())
}
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 {
proxy := f.findAliveProxy()
proxy := f.findAliveProxy(true)
return proxy
}
func (f *Fallback) proxies() []C.Proxy {
func (f *Fallback) proxies(touch bool) []C.Proxy {
elm, _, _ := f.single.Do(func() (interface{}, error) {
return getProvidersProxies(f.providers), nil
return getProvidersProxies(f.providers, touch), nil
})
return elm.([]C.Proxy)
}
func (f *Fallback) findAliveProxy() C.Proxy {
proxies := f.proxies()
func (f *Fallback) findAliveProxy(touch bool) C.Proxy {
proxies := f.proxies(touch)
for _, proxy := range proxies {
if proxy.Alive() {
return proxy
}
}
return f.proxies()[0]
return proxies[0]
}
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 {
proxies := lb.proxies()
proxies := lb.proxies(true)
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) {
return getProvidersProxies(lb.providers), nil
return getProvidersProxies(lb.providers, touch), nil
})
return elm.([]C.Proxy)
@ -145,7 +145,7 @@ func (lb *LoadBalance) proxies() []C.Proxy {
func (lb *LoadBalance) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range lb.proxies() {
for _, proxy := range lb.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{

View File

@ -24,13 +24,16 @@ type GroupCommonOption struct {
Use []string `group:"use,omitempty"`
URL string `group:"url,omitempty"`
Interval int `group:"interval,omitempty"`
Lazy bool `group:"lazy,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) {
decoder := structure.NewDecoder(structure.Option{TagName: "group", WeaklyTypedInput: true})
groupOption := &GroupCommonOption{}
groupOption := &GroupCommonOption{
Lazy: true,
}
if err := decoder.Decode(config, groupOption); err != nil {
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 len(groupOption.Use) != 0 {
hc := provider.NewHealthCheck(ps, "", 0)
hc := provider.NewHealthCheck(ps, "", 0, true)
pd, err := provider.NewCompatibleProvider(groupName, ps, hc)
if err != nil {
return nil, err
@ -69,7 +72,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
// select don't need health check
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)
if err != nil {
return nil, err
@ -82,7 +85,7 @@ func ParseProxyGroup(config map[string]interface{}, proxyMap map[string]C.Proxy,
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)
if err != nil {
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) {
proxies := r.proxies(metadata)
proxies := r.proxies(metadata, true)
if len(proxies) == 0 {
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) {
var all []string
for _, proxy := range r.rawProxies() {
for _, proxy := range r.rawProxies(false) {
all = append(all, proxy.Name())
}
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) {
return getProvidersProxies(r.providers), nil
return getProvidersProxies(r.providers, touch), nil
})
return elm.([]C.Proxy)
}
func (r *Relay) proxies(metadata *C.Metadata) []C.Proxy {
proxies := r.rawProxies()
func (r *Relay) proxies(metadata *C.Metadata, touch bool) []C.Proxy {
proxies := r.rawProxies(touch)
for n, proxy := range proxies {
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) {
c, err := s.selectedProxy().DialContext(ctx, metadata)
c, err := s.selectedProxy(true).DialContext(ctx, metadata)
if err == nil {
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) {
pc, err := s.selectedProxy().DialUDP(metadata)
pc, err := s.selectedProxy(true).DialUDP(metadata)
if err == nil {
pc.AppendToChains(s)
}
@ -40,12 +40,12 @@ func (s *Selector) SupportUDP() bool {
return false
}
return s.selectedProxy().SupportUDP()
return s.selectedProxy(false).SupportUDP()
}
func (s *Selector) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range getProvidersProxies(s.providers) {
for _, proxy := range getProvidersProxies(s.providers, false) {
all = append(all, proxy.Name())
}
@ -57,11 +57,11 @@ func (s *Selector) MarshalJSON() ([]byte, error) {
}
func (s *Selector) Now() string {
return s.selectedProxy().Name()
return s.selectedProxy(false).Name()
}
func (s *Selector) Set(name string) error {
for _, proxy := range getProvidersProxies(s.providers) {
for _, proxy := range getProvidersProxies(s.providers, false) {
if proxy.Name() == name {
s.selected = name
s.single.Reset()
@ -73,12 +73,12 @@ func (s *Selector) Set(name string) error {
}
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) {
proxies := getProvidersProxies(s.providers)
proxies := getProvidersProxies(s.providers, touch)
for _, proxy := range proxies {
if proxy.Name() == s.selected {
return proxy, nil

View File

@ -30,11 +30,11 @@ type URLTest struct {
}
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) {
c, err = u.fast().DialContext(ctx, metadata)
c, err = u.fast(true).DialContext(ctx, metadata)
if err == nil {
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) {
pc, err := u.fast().DialUDP(metadata)
pc, err := u.fast(true).DialUDP(metadata)
if err == nil {
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 {
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) {
return getProvidersProxies(u.providers), nil
return getProvidersProxies(u.providers, touch), nil
})
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) {
proxies := u.proxies()
proxies := u.proxies(touch)
fast := proxies[0]
min := fast.LastDelay()
for _, proxy := range proxies[1:] {
@ -94,12 +94,12 @@ func (u *URLTest) SupportUDP() bool {
return false
}
return u.fast().SupportUDP()
return u.fast(false).SupportUDP()
}
func (u *URLTest) MarshalJSON() ([]byte, error) {
var all []string
for _, proxy := range u.proxies() {
for _, proxy := range u.proxies(false) {
all = append(all, proxy.Name())
}
return json.Marshal(map[string]interface{}{

View File

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

View File

@ -17,6 +17,7 @@ type healthCheckSchema struct {
Enable bool `provider:"enable"`
URL string `provider:"url"`
Interval int `provider:"interval"`
Lazy bool `provider:"lazy,omitempty"`
}
type proxyProviderSchema struct {
@ -30,7 +31,11 @@ type proxyProviderSchema struct {
func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvider, error) {
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 {
return nil, err
}
@ -39,7 +44,7 @@ func ParseProxyProvider(name string, mapping map[string]interface{}) (ProxyProvi
if schema.HealthCheck.Enable {
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)

View File

@ -50,6 +50,9 @@ type Provider interface {
type ProxyProvider interface {
Provider
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()
}
@ -112,6 +115,11 @@ func (pp *proxySetProvider) Proxies() []C.Proxy {
return pp.proxies
}
func (pp *proxySetProvider) ProxiesWithTouch() []C.Proxy {
pp.healthCheck.touch()
return pp.Proxies()
}
func proxiesParse(buf []byte) (interface{}, error) {
schema := &ProxySchema{}
@ -223,6 +231,11 @@ func (cp *compatibleProvider) Proxies() []C.Proxy {
return cp.proxies
}
func (cp *compatibleProvider) ProxiesWithTouch() []C.Proxy {
cp.healthCheck.touch()
return cp.Proxies()
}
func stopCompatibleProvider(pd *CompatibleProvider) {
pd.healthCheck.close()
}

View File

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