github.com/metacubex/mihomo@v1.18.5/adapter/provider/healthcheck.go (about)

     1  package provider
     2  
     3  import (
     4  	"context"
     5  	"strings"
     6  	"sync"
     7  	"time"
     8  
     9  	"github.com/metacubex/mihomo/common/atomic"
    10  	"github.com/metacubex/mihomo/common/batch"
    11  	"github.com/metacubex/mihomo/common/singledo"
    12  	"github.com/metacubex/mihomo/common/utils"
    13  	C "github.com/metacubex/mihomo/constant"
    14  	"github.com/metacubex/mihomo/log"
    15  
    16  	"github.com/dlclark/regexp2"
    17  )
    18  
    19  type HealthCheckOption struct {
    20  	URL      string
    21  	Interval uint
    22  }
    23  
    24  type extraOption struct {
    25  	expectedStatus utils.IntRanges[uint16]
    26  	filters        map[string]struct{}
    27  }
    28  
    29  type HealthCheck struct {
    30  	url            string
    31  	extra          map[string]*extraOption
    32  	mu             sync.Mutex
    33  	started        atomic.Bool
    34  	proxies        []C.Proxy
    35  	interval       time.Duration
    36  	lazy           bool
    37  	expectedStatus utils.IntRanges[uint16]
    38  	lastTouch      atomic.TypedValue[time.Time]
    39  	done           chan struct{}
    40  	singleDo       *singledo.Single[struct{}]
    41  	timeout        time.Duration
    42  }
    43  
    44  func (hc *HealthCheck) process() {
    45  	if hc.started.Load() {
    46  		log.Warnln("Skip start health check timer due to it's started")
    47  		return
    48  	}
    49  
    50  	ticker := time.NewTicker(hc.interval)
    51  	hc.start()
    52  	for {
    53  		select {
    54  		case <-ticker.C:
    55  			lastTouch := hc.lastTouch.Load()
    56  			since := time.Since(lastTouch)
    57  			if !hc.lazy || since < hc.interval {
    58  				hc.check()
    59  			} else {
    60  				log.Debugln("Skip once health check because we are lazy")
    61  			}
    62  		case <-hc.done:
    63  			ticker.Stop()
    64  			hc.stop()
    65  			return
    66  		}
    67  	}
    68  }
    69  
    70  func (hc *HealthCheck) setProxy(proxies []C.Proxy) {
    71  	hc.proxies = proxies
    72  }
    73  
    74  func (hc *HealthCheck) registerHealthCheckTask(url string, expectedStatus utils.IntRanges[uint16], filter string, interval uint) {
    75  	url = strings.TrimSpace(url)
    76  	if len(url) == 0 || url == hc.url {
    77  		log.Debugln("ignore invalid health check url: %s", url)
    78  		return
    79  	}
    80  
    81  	hc.mu.Lock()
    82  	defer hc.mu.Unlock()
    83  
    84  	// if the provider has not set up health checks, then modify it to be the same as the group's interval
    85  	if hc.interval == 0 {
    86  		hc.interval = time.Duration(interval) * time.Second
    87  	}
    88  
    89  	if hc.extra == nil {
    90  		hc.extra = make(map[string]*extraOption)
    91  	}
    92  
    93  	// prioritize the use of previously registered configurations, especially those from provider
    94  	if _, ok := hc.extra[url]; ok {
    95  		// provider default health check does not set filter
    96  		if url != hc.url && len(filter) != 0 {
    97  			splitAndAddFiltersToExtra(filter, hc.extra[url])
    98  		}
    99  
   100  		log.Debugln("health check url: %s exists", url)
   101  		return
   102  	}
   103  
   104  	option := &extraOption{filters: map[string]struct{}{}, expectedStatus: expectedStatus}
   105  	splitAndAddFiltersToExtra(filter, option)
   106  	hc.extra[url] = option
   107  
   108  	if hc.auto() && !hc.started.Load() {
   109  		go hc.process()
   110  	}
   111  }
   112  
   113  func splitAndAddFiltersToExtra(filter string, option *extraOption) {
   114  	filter = strings.TrimSpace(filter)
   115  	if len(filter) != 0 {
   116  		for _, regex := range strings.Split(filter, "`") {
   117  			regex = strings.TrimSpace(regex)
   118  			if len(regex) != 0 {
   119  				option.filters[regex] = struct{}{}
   120  			}
   121  		}
   122  	}
   123  }
   124  
   125  func (hc *HealthCheck) auto() bool {
   126  	return hc.interval != 0
   127  }
   128  
   129  func (hc *HealthCheck) touch() {
   130  	hc.lastTouch.Store(time.Now())
   131  }
   132  
   133  func (hc *HealthCheck) start() {
   134  	hc.started.Store(true)
   135  }
   136  
   137  func (hc *HealthCheck) stop() {
   138  	hc.started.Store(false)
   139  }
   140  
   141  func (hc *HealthCheck) check() {
   142  	if len(hc.proxies) == 0 {
   143  		return
   144  	}
   145  
   146  	_, _, _ = hc.singleDo.Do(func() (struct{}, error) {
   147  		id := utils.NewUUIDV4().String()
   148  		log.Debugln("Start New Health Checking {%s}", id)
   149  		b, _ := batch.New[bool](context.Background(), batch.WithConcurrencyNum[bool](10))
   150  
   151  		// execute default health check
   152  		option := &extraOption{filters: nil, expectedStatus: hc.expectedStatus}
   153  		hc.execute(b, hc.url, id, option)
   154  
   155  		// execute extra health check
   156  		if len(hc.extra) != 0 {
   157  			for url, option := range hc.extra {
   158  				hc.execute(b, url, id, option)
   159  			}
   160  		}
   161  		b.Wait()
   162  		log.Debugln("Finish A Health Checking {%s}", id)
   163  		return struct{}{}, nil
   164  	})
   165  }
   166  
   167  func (hc *HealthCheck) execute(b *batch.Batch[bool], url, uid string, option *extraOption) {
   168  	url = strings.TrimSpace(url)
   169  	if len(url) == 0 {
   170  		log.Debugln("Health Check has been skipped due to testUrl is empty, {%s}", uid)
   171  		return
   172  	}
   173  
   174  	var filterReg *regexp2.Regexp
   175  	var expectedStatus utils.IntRanges[uint16]
   176  	if option != nil {
   177  		expectedStatus = option.expectedStatus
   178  		if len(option.filters) != 0 {
   179  			filters := make([]string, 0, len(option.filters))
   180  			for filter := range option.filters {
   181  				filters = append(filters, filter)
   182  			}
   183  
   184  			filterReg = regexp2.MustCompile(strings.Join(filters, "|"), regexp2.None)
   185  		}
   186  	}
   187  
   188  	for _, proxy := range hc.proxies {
   189  		// skip proxies that do not require health check
   190  		if filterReg != nil {
   191  			if match, _ := filterReg.MatchString(proxy.Name()); !match {
   192  				continue
   193  			}
   194  		}
   195  
   196  		p := proxy
   197  		b.Go(p.Name(), func() (bool, error) {
   198  			ctx, cancel := context.WithTimeout(context.Background(), hc.timeout)
   199  			defer cancel()
   200  			log.Debugln("Health Checking, proxy: %s, url: %s, id: {%s}", p.Name(), url, uid)
   201  			_, _ = p.URLTest(ctx, url, expectedStatus)
   202  			log.Debugln("Health Checked, proxy: %s, url: %s, alive: %t, delay: %d ms uid: {%s}", p.Name(), url, p.AliveForTestUrl(url), p.LastDelayForTestUrl(url), uid)
   203  			return false, nil
   204  		})
   205  	}
   206  }
   207  
   208  func (hc *HealthCheck) close() {
   209  	hc.done <- struct{}{}
   210  }
   211  
   212  func NewHealthCheck(proxies []C.Proxy, url string, timeout uint, interval uint, lazy bool, expectedStatus utils.IntRanges[uint16]) *HealthCheck {
   213  	if url == "" {
   214  		expectedStatus = nil
   215  		interval = 0
   216  	}
   217  	if timeout == 0 {
   218  		timeout = 5000
   219  	}
   220  
   221  	return &HealthCheck{
   222  		proxies:        proxies,
   223  		url:            url,
   224  		timeout:        time.Duration(timeout) * time.Millisecond,
   225  		extra:          map[string]*extraOption{},
   226  		interval:       time.Duration(interval) * time.Second,
   227  		lazy:           lazy,
   228  		expectedStatus: expectedStatus,
   229  		done:           make(chan struct{}, 1),
   230  		singleDo:       singledo.NewSingle[struct{}](time.Second),
   231  	}
   232  }