github.com/network-quality/goresponsiveness@v0.0.0-20240129151524-343954285090/lgc/upload.go (about) 1 /* 2 * This file is part of Go Responsiveness. 3 * 4 * Go Responsiveness is free software: you can redistribute it and/or modify it under 5 * the terms of the GNU General Public License as published by the Free Software Foundation, 6 * either version 2 of the License, or (at your option) any later version. 7 * Go Responsiveness is distributed in the hope that it will be useful, but WITHOUT ANY 8 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A 9 * PARTICULAR PURPOSE. See the GNU General Public License for more details. 10 * 11 * You should have received a copy of the GNU General Public License along 12 * with Go Responsiveness. If not, see <https://www.gnu.org/licenses/>. 13 */ 14 15 package lgc 16 17 import ( 18 "context" 19 "crypto/tls" 20 "fmt" 21 "io" 22 "net/http" 23 "net/http/httptrace" 24 "os" 25 "sync" 26 "sync/atomic" 27 "time" 28 29 "github.com/network-quality/goresponsiveness/debug" 30 "github.com/network-quality/goresponsiveness/l4s" 31 "github.com/network-quality/goresponsiveness/stats" 32 "github.com/network-quality/goresponsiveness/traceable" 33 "github.com/network-quality/goresponsiveness/utilities" 34 ) 35 36 // TODO: All 64-bit fields that are accessed atomically must 37 // appear at the top of this struct. 38 type LoadGeneratingConnectionUpload struct { 39 uploaded uint64 40 lastIntervalEnd int64 41 URL string 42 ConnectToAddr string 43 uploadStartTime time.Time 44 client *http.Client 45 debug debug.DebugLevel 46 InsecureSkipVerify bool 47 KeyLogger io.Writer 48 clientId uint64 49 tracer *httptrace.ClientTrace 50 stats stats.TraceStats 51 status LgcStatus 52 congestionControl *string 53 statusLock *sync.Mutex 54 statusWaiter *sync.Cond 55 } 56 57 func NewLoadGeneratingConnectionUpload(url string, keyLogger io.Writer, connectToAddr string, 58 insecureSkipVerify bool, congestionControl *string, 59 ) LoadGeneratingConnectionUpload { 60 lgu := LoadGeneratingConnectionUpload{ 61 URL: url, 62 KeyLogger: keyLogger, 63 ConnectToAddr: connectToAddr, 64 InsecureSkipVerify: insecureSkipVerify, 65 congestionControl: congestionControl, 66 statusLock: &sync.Mutex{}, 67 } 68 lgu.status = LGC_STATUS_NOT_STARTED 69 lgu.statusWaiter = sync.NewCond(lgu.statusLock) 70 return lgu 71 } 72 73 func (lgu *LoadGeneratingConnectionUpload) WaitUntilStarted(ctxt context.Context) bool { 74 conditional := func() bool { return lgu.status != LGC_STATUS_NOT_STARTED } 75 go utilities.ContextSignaler(ctxt, 500*time.Millisecond, &conditional, lgu.statusWaiter) 76 return utilities.WaitWithContext(ctxt, &conditional, lgu.statusLock, lgu.statusWaiter) 77 } 78 79 func (lgu *LoadGeneratingConnectionUpload) SetDnsStartTimeInfo( 80 now time.Time, 81 dnsStartInfo httptrace.DNSStartInfo, 82 ) { 83 lgu.stats.DnsStartTime = now 84 lgu.stats.DnsStart = dnsStartInfo 85 if debug.IsDebug(lgu.debug) { 86 fmt.Printf( 87 "DNS Start for %v: %v\n", 88 lgu.ClientId(), 89 dnsStartInfo, 90 ) 91 } 92 } 93 94 func (lgu *LoadGeneratingConnectionUpload) SetDnsDoneTimeInfo( 95 now time.Time, 96 dnsDoneInfo httptrace.DNSDoneInfo, 97 ) { 98 lgu.stats.DnsDoneTime = now 99 lgu.stats.DnsDone = dnsDoneInfo 100 if debug.IsDebug(lgu.debug) { 101 fmt.Printf( 102 "DNS Done for %v: %v\n", 103 lgu.ClientId(), 104 lgu.stats.DnsDone, 105 ) 106 } 107 } 108 109 func (lgu *LoadGeneratingConnectionUpload) SetConnectStartTime( 110 now time.Time, 111 ) { 112 lgu.stats.ConnectStartTime = now 113 if debug.IsDebug(lgu.debug) { 114 fmt.Printf( 115 "TCP Start for %v at %v\n", 116 lgu.ClientId(), 117 lgu.stats.ConnectStartTime, 118 ) 119 } 120 } 121 122 func (lgu *LoadGeneratingConnectionUpload) SetConnectDoneTimeError( 123 now time.Time, 124 err error, 125 ) { 126 lgu.stats.ConnectDoneTime = now 127 lgu.stats.ConnectDoneError = err 128 if debug.IsDebug(lgu.debug) { 129 fmt.Printf( 130 "TCP Done for %v (with error %v) @ %v\n", 131 lgu.ClientId(), 132 lgu.stats.ConnectDoneError, 133 lgu.stats.ConnectDoneTime, 134 ) 135 } 136 } 137 138 func (lgu *LoadGeneratingConnectionUpload) SetGetConnTime(now time.Time) { 139 lgu.stats.GetConnectionStartTime = now 140 if debug.IsDebug(lgu.debug) { 141 fmt.Printf( 142 "Started getting connection for %v @ %v\n", 143 lgu.ClientId(), 144 lgu.stats.GetConnectionStartTime, 145 ) 146 } 147 } 148 149 func (lgu *LoadGeneratingConnectionUpload) SetGotConnTimeInfo( 150 now time.Time, 151 gotConnInfo httptrace.GotConnInfo, 152 ) { 153 if gotConnInfo.Reused { 154 fmt.Printf("Unexpectedly reusing a connection!\n") 155 panic(!gotConnInfo.Reused) 156 } 157 lgu.stats.GetConnectionDoneTime = now 158 lgu.stats.ConnInfo = gotConnInfo 159 if debug.IsDebug(lgu.debug) { 160 fmt.Printf( 161 "Got connection for %v at %v with info %v\n", 162 lgu.ClientId(), 163 lgu.stats.GetConnectionDoneTime, 164 lgu.stats.ConnInfo, 165 ) 166 } 167 168 if lgu.congestionControl != nil { 169 if debug.IsDebug(lgu.debug) { 170 fmt.Printf( 171 "Attempting to set congestion control algorithm to %v for connection %v at %v with info %v\n", 172 *lgu.congestionControl, 173 lgu.ClientId(), 174 lgu.stats.GetConnectionDoneTime, 175 lgu.stats.ConnInfo, 176 ) 177 } 178 if err := l4s.SetL4S(lgu.stats.ConnInfo.Conn, lgu.congestionControl); err != nil { 179 fmt.Fprintf( 180 os.Stderr, 181 "Error setting L4S for %v at %v: %v\n", 182 lgu.ClientId(), 183 lgu.stats.GetConnectionDoneTime, 184 err.Error(), 185 ) 186 } 187 } 188 } 189 190 func (lgu *LoadGeneratingConnectionUpload) SetTLSHandshakeStartTime( 191 now time.Time, 192 ) { 193 lgu.stats.TLSStartTime = utilities.Some(now) 194 if debug.IsDebug(lgu.debug) { 195 fmt.Printf( 196 "Started TLS Handshake for %v @ %v\n", 197 lgu.ClientId(), 198 lgu.stats.TLSStartTime, 199 ) 200 } 201 } 202 203 func (lgu *LoadGeneratingConnectionUpload) SetTLSHandshakeDoneTimeState( 204 now time.Time, 205 connectionState tls.ConnectionState, 206 ) { 207 lgu.stats.TLSDoneTime = utilities.Some(now) 208 lgu.stats.TLSConnInfo = connectionState 209 if debug.IsDebug(lgu.debug) { 210 fmt.Printf( 211 "Completed TLS handshake for %v at %v with info %v\n", 212 lgu.ClientId(), 213 lgu.stats.TLSDoneTime, 214 lgu.stats.TLSConnInfo, 215 ) 216 } 217 } 218 219 func (lgu *LoadGeneratingConnectionUpload) SetHttpWroteRequestTimeInfo( 220 now time.Time, 221 info httptrace.WroteRequestInfo, 222 ) { 223 lgu.stats.HttpWroteRequestTime = now 224 lgu.stats.HttpInfo = info 225 if debug.IsDebug(lgu.debug) { 226 fmt.Printf( 227 "(lgu) Http finished writing request for %v at %v with info %v\n", 228 lgu.ClientId(), 229 lgu.stats.HttpWroteRequestTime, 230 lgu.stats.HttpInfo, 231 ) 232 } 233 } 234 235 func (lgu *LoadGeneratingConnectionUpload) SetHttpResponseReadyTime( 236 now time.Time, 237 ) { 238 lgu.stats.HttpResponseReadyTime = now 239 if debug.IsDebug(lgu.debug) { 240 fmt.Printf( 241 "Got the first byte of HTTP response headers for %v at %v\n", 242 lgu.ClientId(), 243 lgu.stats.HttpResponseReadyTime, 244 ) 245 } 246 } 247 248 func (lgu *LoadGeneratingConnectionUpload) ClientId() uint64 { 249 return lgu.clientId 250 } 251 252 func (lgu *LoadGeneratingConnectionUpload) TransferredInInterval() (uint64, time.Duration) { 253 transferred := atomic.SwapUint64(&lgu.uploaded, 0) 254 newIntervalEnd := (time.Now().Sub(lgu.uploadStartTime)).Nanoseconds() 255 previousIntervalEnd := atomic.SwapInt64(&lgu.lastIntervalEnd, newIntervalEnd) 256 intervalLength := time.Duration(newIntervalEnd - previousIntervalEnd) 257 if debug.IsDebug(lgu.debug) { 258 fmt.Printf("upload: Transferred: %v bytes in %v.\n", transferred, intervalLength) 259 } 260 return transferred, intervalLength 261 } 262 263 func (lgu *LoadGeneratingConnectionUpload) Client() *http.Client { 264 return lgu.client 265 } 266 267 func (lgu *LoadGeneratingConnectionUpload) Status() LgcStatus { 268 return lgu.status 269 } 270 271 func (lgd *LoadGeneratingConnectionUpload) Direction() LgcDirection { 272 return LGC_UP 273 } 274 275 type syntheticCountingReader struct { 276 n *uint64 277 ctx context.Context 278 lgu *LoadGeneratingConnectionUpload 279 } 280 281 func (s *syntheticCountingReader) Read(p []byte) (n int, err error) { 282 if s.ctx.Err() != nil { 283 return 0, io.EOF 284 } 285 if *s.n == 0 { 286 s.lgu.statusLock.Lock() 287 s.lgu.status = LGC_STATUS_RUNNING 288 s.lgu.statusWaiter.Broadcast() 289 s.lgu.statusLock.Unlock() 290 } 291 err = nil 292 n = len(p) 293 294 atomic.AddUint64(s.n, uint64(n)) 295 return 296 } 297 298 func (lgu *LoadGeneratingConnectionUpload) doUpload(ctx context.Context) error { 299 lgu.uploaded = 0 300 s := &syntheticCountingReader{n: &lgu.uploaded, ctx: ctx, lgu: lgu} 301 var resp *http.Response = nil 302 var request *http.Request = nil 303 var err error 304 305 if request, err = http.NewRequestWithContext( 306 httptrace.WithClientTrace(ctx, lgu.tracer), 307 "POST", 308 lgu.URL, 309 s, 310 ); err != nil { 311 lgu.statusLock.Lock() 312 lgu.status = LGC_STATUS_ERROR 313 lgu.statusWaiter.Broadcast() 314 lgu.statusLock.Unlock() 315 return err 316 } 317 318 // Used to disable compression 319 request.Header.Set("Accept-Encoding", "identity") 320 request.Header.Set("User-Agent", utilities.UserAgent()) 321 322 lgu.uploadStartTime = time.Now() 323 lgu.lastIntervalEnd = 0 324 325 if resp, err = lgu.client.Do(request); err != nil { 326 lgu.statusLock.Lock() 327 lgu.status = LGC_STATUS_ERROR 328 lgu.statusWaiter.Broadcast() 329 lgu.statusLock.Unlock() 330 return err 331 } 332 333 lgu.statusLock.Lock() 334 lgu.status = LGC_STATUS_DONE 335 lgu.statusWaiter.Broadcast() 336 lgu.statusLock.Unlock() 337 338 resp.Body.Close() 339 if debug.IsDebug(lgu.debug) { 340 fmt.Printf("Ending a load-generating upload.\n") 341 } 342 return nil 343 } 344 345 func (lgu *LoadGeneratingConnectionUpload) Start( 346 parentCtx context.Context, 347 debugLevel debug.DebugLevel, 348 ) bool { 349 lgu.uploaded = 0 350 lgu.clientId = utilities.GenerateUniqueId() 351 lgu.debug = debugLevel 352 353 transport := &http.Transport{ 354 Proxy: http.ProxyFromEnvironment, 355 TLSClientConfig: &tls.Config{ 356 InsecureSkipVerify: lgu.InsecureSkipVerify, 357 }, 358 } 359 360 if !utilities.IsInterfaceNil(lgu.KeyLogger) { 361 if debug.IsDebug(lgu.debug) { 362 fmt.Printf( 363 "Using an SSL Key Logger for this load-generating upload.\n", 364 ) 365 } 366 transport.TLSClientConfig.KeyLogWriter = lgu.KeyLogger 367 } 368 369 utilities.OverrideHostTransport(transport, lgu.ConnectToAddr) 370 371 lgu.client = &http.Client{Transport: transport} 372 lgu.tracer = traceable.GenerateHttpTimingTracer(lgu, lgu.debug) 373 374 if debug.IsDebug(lgu.debug) { 375 fmt.Printf("Started a load-generating upload (id: %v).\n", lgu.clientId) 376 } 377 378 go lgu.doUpload(parentCtx) 379 return true 380 } 381 382 func (lgu *LoadGeneratingConnectionUpload) Stats() *stats.TraceStats { 383 return &lgu.stats 384 }