github.com/imannamdari/v2ray-core/v5@v5.0.5/app/observatory/burst/healthping.go (about)

     1  package burst
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"strings"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/imannamdari/v2ray-core/v5/common/dice"
    11  )
    12  
    13  // HealthPingSettings holds settings for health Checker
    14  type HealthPingSettings struct {
    15  	Destination   string        `json:"destination"`
    16  	Connectivity  string        `json:"connectivity"`
    17  	Interval      time.Duration `json:"interval"`
    18  	SamplingCount int           `json:"sampling"`
    19  	Timeout       time.Duration `json:"timeout"`
    20  }
    21  
    22  // HealthPing is the health checker for balancers
    23  type HealthPing struct {
    24  	ctx         context.Context
    25  	access      sync.Mutex
    26  	ticker      *time.Ticker
    27  	tickerClose chan struct{}
    28  
    29  	Settings *HealthPingSettings
    30  	Results  map[string]*HealthPingRTTS
    31  }
    32  
    33  // NewHealthPing creates a new HealthPing with settings
    34  func NewHealthPing(ctx context.Context, config *HealthPingConfig) *HealthPing {
    35  	settings := &HealthPingSettings{}
    36  	if config != nil {
    37  		settings = &HealthPingSettings{
    38  			Connectivity:  strings.TrimSpace(config.Connectivity),
    39  			Destination:   strings.TrimSpace(config.Destination),
    40  			Interval:      time.Duration(config.Interval),
    41  			SamplingCount: int(config.SamplingCount),
    42  			Timeout:       time.Duration(config.Timeout),
    43  		}
    44  	}
    45  	if settings.Destination == "" {
    46  		// Destination URL, need 204 for success return default to chromium
    47  		// https://github.com/chromium/chromium/blob/main/components/safety_check/url_constants.cc#L10
    48  		// https://chromium.googlesource.com/chromium/src/+/refs/heads/main/components/safety_check/url_constants.cc#10
    49  		settings.Destination = "https://connectivitycheck.gstatic.com/generate_204"
    50  	}
    51  	if settings.Interval == 0 {
    52  		settings.Interval = time.Duration(1) * time.Minute
    53  	} else if settings.Interval < 10 {
    54  		newError("health check interval is too small, 10s is applied").AtWarning().WriteToLog()
    55  		settings.Interval = time.Duration(10) * time.Second
    56  	}
    57  	if settings.SamplingCount <= 0 {
    58  		settings.SamplingCount = 10
    59  	}
    60  	if settings.Timeout <= 0 {
    61  		// results are saved after all health pings finish,
    62  		// a larger timeout could possibly makes checks run longer
    63  		settings.Timeout = time.Duration(5) * time.Second
    64  	}
    65  	return &HealthPing{
    66  		ctx:      ctx,
    67  		Settings: settings,
    68  		Results:  nil,
    69  	}
    70  }
    71  
    72  // StartScheduler implements the HealthChecker
    73  func (h *HealthPing) StartScheduler(selector func() ([]string, error)) {
    74  	if h.ticker != nil {
    75  		return
    76  	}
    77  	interval := h.Settings.Interval * time.Duration(h.Settings.SamplingCount)
    78  	ticker := time.NewTicker(interval)
    79  	tickerClose := make(chan struct{})
    80  	h.ticker = ticker
    81  	h.tickerClose = tickerClose
    82  	go func() {
    83  		for {
    84  			go func() {
    85  				tags, err := selector()
    86  				if err != nil {
    87  					newError("error select outbounds for scheduled health check: ", err).AtWarning().WriteToLog()
    88  					return
    89  				}
    90  				h.doCheck(tags, interval, h.Settings.SamplingCount)
    91  				h.Cleanup(tags)
    92  			}()
    93  			select {
    94  			case <-ticker.C:
    95  				continue
    96  			case <-tickerClose:
    97  				return
    98  			}
    99  		}
   100  	}()
   101  }
   102  
   103  // StopScheduler implements the HealthChecker
   104  func (h *HealthPing) StopScheduler() {
   105  	if h.ticker == nil {
   106  		return
   107  	}
   108  	h.ticker.Stop()
   109  	h.ticker = nil
   110  	close(h.tickerClose)
   111  	h.tickerClose = nil
   112  }
   113  
   114  // Check implements the HealthChecker
   115  func (h *HealthPing) Check(tags []string) error {
   116  	if len(tags) == 0 {
   117  		return nil
   118  	}
   119  	newError("perform one-time health check for tags ", tags).AtInfo().WriteToLog()
   120  	h.doCheck(tags, 0, 1)
   121  	return nil
   122  }
   123  
   124  type rtt struct {
   125  	handler string
   126  	value   time.Duration
   127  }
   128  
   129  // doCheck performs the 'rounds' amount checks in given 'duration'. You should make
   130  // sure all tags are valid for current balancer
   131  func (h *HealthPing) doCheck(tags []string, duration time.Duration, rounds int) {
   132  	count := len(tags) * rounds
   133  	if count == 0 {
   134  		return
   135  	}
   136  	ch := make(chan *rtt, count)
   137  
   138  	for _, tag := range tags {
   139  		handler := tag
   140  		client := newPingClient(
   141  			h.ctx,
   142  			h.Settings.Destination,
   143  			h.Settings.Timeout,
   144  			handler,
   145  		)
   146  		for i := 0; i < rounds; i++ {
   147  			delay := time.Duration(0)
   148  			if duration > 0 {
   149  				delay = time.Duration(dice.Roll(int(duration)))
   150  			}
   151  			time.AfterFunc(delay, func() {
   152  				newError("checking ", handler).AtDebug().WriteToLog()
   153  				delay, err := client.MeasureDelay()
   154  				if err == nil {
   155  					ch <- &rtt{
   156  						handler: handler,
   157  						value:   delay,
   158  					}
   159  					return
   160  				}
   161  				if !h.checkConnectivity() {
   162  					newError("network is down").AtWarning().WriteToLog()
   163  					ch <- &rtt{
   164  						handler: handler,
   165  						value:   0,
   166  					}
   167  					return
   168  				}
   169  				newError(fmt.Sprintf(
   170  					"error ping %s with %s: %s",
   171  					h.Settings.Destination,
   172  					handler,
   173  					err,
   174  				)).AtWarning().WriteToLog()
   175  				ch <- &rtt{
   176  					handler: handler,
   177  					value:   rttFailed,
   178  				}
   179  			})
   180  		}
   181  	}
   182  	for i := 0; i < count; i++ {
   183  		rtt := <-ch
   184  		if rtt.value > 0 {
   185  			// should not put results when network is down
   186  			h.PutResult(rtt.handler, rtt.value)
   187  		}
   188  	}
   189  }
   190  
   191  // PutResult puts a ping rtt to results
   192  func (h *HealthPing) PutResult(tag string, rtt time.Duration) {
   193  	h.access.Lock()
   194  	defer h.access.Unlock()
   195  	if h.Results == nil {
   196  		h.Results = make(map[string]*HealthPingRTTS)
   197  	}
   198  	r, ok := h.Results[tag]
   199  	if !ok {
   200  		// validity is 2 times to sampling period, since the check are
   201  		// distributed in the time line randomly, in extreme cases,
   202  		// previous checks are distributed on the left, and latters
   203  		// on the right
   204  		validity := h.Settings.Interval * time.Duration(h.Settings.SamplingCount) * 2
   205  		r = NewHealthPingResult(h.Settings.SamplingCount, validity)
   206  		h.Results[tag] = r
   207  	}
   208  	r.Put(rtt)
   209  }
   210  
   211  // Cleanup removes results of removed handlers,
   212  // tags should be all valid tags of the Balancer now
   213  func (h *HealthPing) Cleanup(tags []string) {
   214  	h.access.Lock()
   215  	defer h.access.Unlock()
   216  	for tag := range h.Results {
   217  		found := false
   218  		for _, v := range tags {
   219  			if tag == v {
   220  				found = true
   221  				break
   222  			}
   223  		}
   224  		if !found {
   225  			delete(h.Results, tag)
   226  		}
   227  	}
   228  }
   229  
   230  // checkConnectivity checks the network connectivity, it returns
   231  // true if network is good or "connectivity check url" not set
   232  func (h *HealthPing) checkConnectivity() bool {
   233  	if h.Settings.Connectivity == "" {
   234  		return true
   235  	}
   236  	tester := newDirectPingClient(
   237  		h.Settings.Connectivity,
   238  		h.Settings.Timeout,
   239  	)
   240  	if _, err := tester.MeasureDelay(); err != nil {
   241  		return false
   242  	}
   243  	return true
   244  }