github.com/v2fly/v2ray-core/v5@v5.16.2-0.20240507031116-8191faa6e095/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/v2fly/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 }