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  }