github.com/network-quality/goresponsiveness@v0.0.0-20240129151524-343954285090/probe/probe.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 probe 16 17 import ( 18 "context" 19 "fmt" 20 "io" 21 "net/http" 22 "net/http/httptrace" 23 "os" 24 "time" 25 26 "github.com/network-quality/goresponsiveness/constants" 27 "github.com/network-quality/goresponsiveness/debug" 28 "github.com/network-quality/goresponsiveness/extendedstats" 29 "github.com/network-quality/goresponsiveness/utilities" 30 ) 31 32 type ProbeType int64 33 34 type ProbeConfiguration struct { 35 ConnectToAddr string 36 URL string 37 Host string 38 CongestionControl *string 39 InsecureSkipVerify bool 40 } 41 42 type ProbeDataPoint struct { 43 Time time.Time `Description:"Time of the generation of the data point." Formatter:"Format" FormatterArgument:"01-02-2006-15-04-05.000"` 44 RoundTripCount uint64 `Description:"The number of round trips measured by this data point."` 45 Duration time.Duration `Description:"The duration for this measurement." Formatter:"Seconds"` 46 TCPRtt time.Duration `Description:"The underlying connection's RTT at probe time." Formatter:"Seconds"` 47 TCPCwnd uint32 `Description:"The underlying connection's congestion window at probe time."` 48 Type ProbeType `Description:"The type of the probe." Formatter:"Value"` 49 CongestionControl string `Description:"The congestion control algorithm used."` 50 } 51 52 const ( 53 SelfUp ProbeType = iota 54 SelfDown 55 Foreign 56 ) 57 58 type ProbeRoundTripCountType uint16 59 60 const ( 61 DefaultDownRoundTripCount ProbeRoundTripCountType = 1 62 SelfUpRoundTripCount ProbeRoundTripCountType = 1 63 SelfDownRoundTripCount ProbeRoundTripCountType = 1 64 ForeignRoundTripCount ProbeRoundTripCountType = 3 65 ) 66 67 func (pt ProbeType) Value() string { 68 if pt == SelfUp { 69 return "SelfUp" 70 } else if pt == SelfDown { 71 return "SelfDown" 72 } 73 return "Foreign" 74 } 75 76 func (pt ProbeType) IsSelf() bool { 77 return pt == SelfUp || pt == SelfDown 78 } 79 80 func Probe( 81 managingCtx context.Context, 82 client *http.Client, 83 probeUrl string, 84 probeHost string, // optional: for use with a test_endpoint 85 probeType ProbeType, 86 probeId uint, 87 congestionControl *string, 88 captureExtendedStats bool, 89 debugging *debug.DebugWithPrefix, 90 ) (*ProbeDataPoint, error) { 91 if client == nil { 92 return nil, fmt.Errorf("cannot start a probe with a nil client") 93 } 94 95 probeTracer := NewProbeTracer(client, probeType, probeId, congestionControl, debugging) 96 time_before_probe := time.Now() 97 probe_req, err := http.NewRequestWithContext( 98 httptrace.WithClientTrace(managingCtx, probeTracer.trace), 99 "GET", 100 probeUrl, 101 nil, 102 ) 103 if err != nil { 104 return nil, err 105 } 106 107 // Used to disable compression 108 probe_req.Header.Set("Accept-Encoding", "identity") 109 probe_req.Header.Set("User-Agent", utilities.UserAgent()) 110 111 probe_resp, err := client.Do(probe_req) 112 if err != nil { 113 if debug.IsDebug(debugging.Level) { 114 fmt.Printf( 115 "(%s) (%s Probe %v) An error occurred during http.Do: %v\n", 116 debugging.Prefix, 117 probeType.Value(), 118 probeId, 119 err, 120 ) 121 } 122 return nil, err 123 } 124 125 // Header.Get returns "" when not set 126 if probe_resp.Header.Get("Content-Encoding") != "" { 127 return nil, fmt.Errorf("Content-Encoding header was set (compression not allowed)") 128 } 129 130 // TODO: Make this interruptable somehow by using _ctx_. 131 _, err = io.ReadAll(probe_resp.Body) 132 if err != nil { 133 if debug.IsDebug(debugging.Level) { 134 fmt.Printf( 135 "(%s) (%s Probe %v) An error occurred during io.ReadAll: %v\n", 136 debugging.Prefix, 137 probeType.Value(), 138 probeId, 139 err, 140 ) 141 } 142 return nil, err 143 } 144 time_after_probe := time.Now() 145 146 // Depending on whether we think that Close() requires another RTT (via TCP), we 147 // may need to move this before/after capturing the after time. 148 probe_resp.Body.Close() 149 150 sanity := time_after_probe.Sub(time_before_probe) 151 152 // When the probe is run on a load-generating connection (a self probe) there should 153 // only be a single round trip that is measured. We will take the accumulation of all these 154 // values just to be sure, though. Because of how this traced connection was launched, most 155 // of the values will be 0 (or very small where the time that go takes for delivering callbacks 156 // and doing context switches pokes through). When it is !isSelfProbe then the values will 157 // be significant and we want to add them regardless! 158 totalDelay := probeTracer.GetTLSAndHttpHeaderDelta() + probeTracer.GetHttpDownloadDelta( 159 time_after_probe, 160 ) + probeTracer.GetTCPDelta() 161 162 // We must have reused the connection if we are a self probe! 163 if probeType.IsSelf() && !probeTracer.stats.ConnectionReused { 164 fmt.Fprintf(os.Stderr, 165 "(%s) (%s Probe %v) Probe should have reused a connection, but it didn't!\n", 166 debugging.Prefix, 167 probeType.Value(), 168 probeId, 169 ) 170 panic(!probeTracer.stats.ConnectionReused) 171 } 172 173 if debug.IsDebug(debugging.Level) { 174 fmt.Printf( 175 "(%s) (%s Probe %v) sanity vs total: %v vs %v\n", 176 debugging.Prefix, 177 probeType.Value(), 178 probeId, 179 sanity, 180 totalDelay, 181 ) 182 } 183 roundTripCount := DefaultDownRoundTripCount 184 if probeType == Foreign { 185 roundTripCount = ForeignRoundTripCount 186 } 187 // Careful!!! It's possible that this channel has been closed because the Prober that 188 // started it has been stopped. Writing to a closed channel will cause a panic. It might not 189 // matter because a panic just stops the go thread containing the paniced code and we are in 190 // a go thread that executes only this function. 191 defer func() { 192 isThreadPanicing := recover() 193 if isThreadPanicing != nil && debug.IsDebug(debugging.Level) { 194 fmt.Printf( 195 "(%s) (%s Probe %v) Probe attempted to write to the result channel after its invoker ended (official reason: %v).\n", 196 debugging.Prefix, 197 probeType.Value(), 198 probeId, 199 isThreadPanicing, 200 ) 201 } 202 }() 203 tcpRtt := time.Duration(0 * time.Second) 204 tcpCwnd := uint32(0) 205 // TODO: Only get the extended stats for a connection if the user has requested them overall. 206 if captureExtendedStats && extendedstats.ExtendedStatsAvailable() { 207 tcpInfo, err := extendedstats.GetTCPInfo(probeTracer.stats.ConnInfo.Conn) 208 if err == nil { 209 tcpRtt = time.Duration(tcpInfo.Rtt) * time.Microsecond 210 tcpCwnd = tcpInfo.Snd_cwnd 211 } else { 212 fmt.Printf("Warning: Could not fetch the extended stats for a probe: %v\n", err) 213 } 214 } 215 return &ProbeDataPoint{ 216 Time: time_before_probe, 217 RoundTripCount: uint64(roundTripCount), 218 Duration: totalDelay, 219 TCPRtt: tcpRtt, 220 TCPCwnd: tcpCwnd, 221 Type: probeType, 222 CongestionControl: *utilities.Conditional(congestionControl == nil, 223 &constants.DefaultL4SCongestionControlAlgorithm, congestionControl), 224 }, nil 225 }