golang.org/x/net@v0.25.1-0.20240516223405-c87a5b62e243/quic/conn_async_test.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  //go:build go1.21
     6  
     7  package quic
     8  
     9  import (
    10  	"context"
    11  	"errors"
    12  	"fmt"
    13  	"path/filepath"
    14  	"runtime"
    15  	"sync"
    16  )
    17  
    18  // asyncTestState permits handling asynchronous operations in a synchronous test.
    19  //
    20  // For example, a test may want to write to a stream and observe that
    21  // STREAM frames are sent with the contents of the write in response
    22  // to MAX_STREAM_DATA frames received from the peer.
    23  // The Stream.Write is an asynchronous operation, but the test is simpler
    24  // if we can start the write, observe the first STREAM frame sent,
    25  // send a MAX_STREAM_DATA frame, observe the next STREAM frame sent, etc.
    26  //
    27  // We do this by instrumenting points where operations can block.
    28  // We start async operations like Write in a goroutine,
    29  // and wait for the operation to either finish or hit a blocking point.
    30  // When the connection event loop is idle, we check a list of
    31  // blocked operations to see if any can be woken.
    32  type asyncTestState struct {
    33  	mu      sync.Mutex
    34  	notify  chan struct{}
    35  	blocked map[*blockedAsync]struct{}
    36  }
    37  
    38  // An asyncOp is an asynchronous operation that results in (T, error).
    39  type asyncOp[T any] struct {
    40  	v   T
    41  	err error
    42  
    43  	caller     string
    44  	tc         *testConn
    45  	donec      chan struct{}
    46  	cancelFunc context.CancelFunc
    47  }
    48  
    49  // cancel cancels the async operation's context, and waits for
    50  // the operation to complete.
    51  func (a *asyncOp[T]) cancel() {
    52  	select {
    53  	case <-a.donec:
    54  		return // already done
    55  	default:
    56  	}
    57  	a.cancelFunc()
    58  	<-a.tc.asyncTestState.notify
    59  	select {
    60  	case <-a.donec:
    61  	default:
    62  		panic(fmt.Errorf("%v: async op failed to finish after being canceled", a.caller))
    63  	}
    64  }
    65  
    66  var errNotDone = errors.New("async op is not done")
    67  
    68  // result returns the result of the async operation.
    69  // It returns errNotDone if the operation is still in progress.
    70  //
    71  // Note that unlike a traditional async/await, this doesn't block
    72  // waiting for the operation to complete. Since tests have full
    73  // control over the progress of operations, an asyncOp can only
    74  // become done in reaction to the test taking some action.
    75  func (a *asyncOp[T]) result() (v T, err error) {
    76  	a.tc.wait()
    77  	select {
    78  	case <-a.donec:
    79  		return a.v, a.err
    80  	default:
    81  		return v, errNotDone
    82  	}
    83  }
    84  
    85  // A blockedAsync is a blocked async operation.
    86  type blockedAsync struct {
    87  	until func() bool   // when this returns true, the operation is unblocked
    88  	donec chan struct{} // closed when the operation is unblocked
    89  }
    90  
    91  type asyncContextKey struct{}
    92  
    93  // runAsync starts an asynchronous operation.
    94  //
    95  // The function f should call a blocking function such as
    96  // Stream.Write or Conn.AcceptStream and return its result.
    97  // It must use the provided context.
    98  func runAsync[T any](tc *testConn, f func(context.Context) (T, error)) *asyncOp[T] {
    99  	as := &tc.asyncTestState
   100  	if as.notify == nil {
   101  		as.notify = make(chan struct{})
   102  		as.mu.Lock()
   103  		as.blocked = make(map[*blockedAsync]struct{})
   104  		as.mu.Unlock()
   105  	}
   106  	_, file, line, _ := runtime.Caller(1)
   107  	ctx := context.WithValue(context.Background(), asyncContextKey{}, true)
   108  	ctx, cancel := context.WithCancel(ctx)
   109  	a := &asyncOp[T]{
   110  		tc:         tc,
   111  		caller:     fmt.Sprintf("%v:%v", filepath.Base(file), line),
   112  		donec:      make(chan struct{}),
   113  		cancelFunc: cancel,
   114  	}
   115  	go func() {
   116  		a.v, a.err = f(ctx)
   117  		close(a.donec)
   118  		as.notify <- struct{}{}
   119  	}()
   120  	tc.t.Cleanup(func() {
   121  		if _, err := a.result(); err == errNotDone {
   122  			tc.t.Errorf("%v: async operation is still executing at end of test", a.caller)
   123  			a.cancel()
   124  		}
   125  	})
   126  	// Wait for the operation to either finish or block.
   127  	<-as.notify
   128  	tc.wait()
   129  	return a
   130  }
   131  
   132  // waitUntil waits for a blocked async operation to complete.
   133  // The operation is complete when the until func returns true.
   134  func (as *asyncTestState) waitUntil(ctx context.Context, until func() bool) error {
   135  	if until() {
   136  		return nil
   137  	}
   138  	if err := ctx.Err(); err != nil {
   139  		// Context has already expired.
   140  		return err
   141  	}
   142  	if ctx.Value(asyncContextKey{}) == nil {
   143  		// Context is not one that we've created, and hasn't expired.
   144  		// This probably indicates that we've tried to perform a
   145  		// blocking operation without using the async test harness here,
   146  		// which may have unpredictable results.
   147  		panic("blocking async point with unexpected Context")
   148  	}
   149  	b := &blockedAsync{
   150  		until: until,
   151  		donec: make(chan struct{}),
   152  	}
   153  	// Record this as a pending blocking operation.
   154  	as.mu.Lock()
   155  	as.blocked[b] = struct{}{}
   156  	as.mu.Unlock()
   157  	// Notify the creator of the operation that we're blocked,
   158  	// and wait to be woken up.
   159  	as.notify <- struct{}{}
   160  	select {
   161  	case <-b.donec:
   162  	case <-ctx.Done():
   163  		return ctx.Err()
   164  	}
   165  	return nil
   166  }
   167  
   168  // wakeAsync tries to wake up a blocked async operation.
   169  // It returns true if one was woken, false otherwise.
   170  func (as *asyncTestState) wakeAsync() bool {
   171  	as.mu.Lock()
   172  	var woken *blockedAsync
   173  	for w := range as.blocked {
   174  		if w.until() {
   175  			woken = w
   176  			delete(as.blocked, w)
   177  			break
   178  		}
   179  	}
   180  	as.mu.Unlock()
   181  	if woken == nil {
   182  		return false
   183  	}
   184  	close(woken.donec)
   185  	<-as.notify // must not hold as.mu while blocked here
   186  	return true
   187  }