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  }