go.uber.org/yarpc@v1.72.1/internal/integrationtest/util.go (about)

     1  // Copyright (c) 2022 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package integrationtest
    22  
    23  import (
    24  	"bytes"
    25  	"context"
    26  	"fmt"
    27  	"net"
    28  	"sync"
    29  	"testing"
    30  	"time"
    31  
    32  	"github.com/stretchr/testify/assert"
    33  	"github.com/stretchr/testify/require"
    34  	"go.uber.org/yarpc"
    35  	"go.uber.org/yarpc/api/peer"
    36  	"go.uber.org/yarpc/api/transport"
    37  	"go.uber.org/yarpc/encoding/raw"
    38  	"go.uber.org/yarpc/internal/testtime"
    39  	peerbind "go.uber.org/yarpc/peer"
    40  	"go.uber.org/yarpc/peer/roundrobin"
    41  )
    42  
    43  const (
    44  	_maxAttempts        = 1000
    45  	_concurrentAttempts = 100
    46  	_unconnectableAddr  = "0.0.0.1:1"
    47  )
    48  
    49  // TransportSpec specifies how to create test clients and servers for a transport.
    50  type TransportSpec struct {
    51  	NewServerTransport func(t *testing.T, addr string) peer.Transport
    52  	NewClientTransport func(t *testing.T) peer.Transport
    53  	NewInbound         func(xport peer.Transport, addr string) transport.Inbound
    54  	NewUnaryOutbound   func(xport peer.Transport, pc peer.Chooser) transport.UnaryOutbound
    55  	Identify           func(addr string) peer.Identifier
    56  	Addr               func(xport peer.Transport, inbound transport.Inbound) string
    57  }
    58  
    59  // Test runs reusable tests with the transport spec.
    60  func (s TransportSpec) Test(t *testing.T) {
    61  	t.Run("reuseConnRoundRobin", s.TestConcurrentClientsRoundRobin)
    62  	t.Run("backoffConnRoundRobin", s.TestBackoffConnRoundRobin)
    63  	t.Run("connectAndStopRoundRobin", s.TestConnectAndStopRoundRobin)
    64  }
    65  
    66  // NewClient returns a running dispatcher and a raw client for the echo
    67  // procedure.
    68  func (s TransportSpec) NewClient(t *testing.T, addrs []string) (*yarpc.Dispatcher, raw.Client) {
    69  	ids := make([]peer.Identifier, len(addrs))
    70  	for i, addr := range addrs {
    71  		ids[i] = s.Identify(addr)
    72  	}
    73  
    74  	xport := s.NewClientTransport(t)
    75  
    76  	pl := roundrobin.New(xport)
    77  	pc := peerbind.Bind(pl, peerbind.BindPeers(ids))
    78  	ob := s.NewUnaryOutbound(xport, pc)
    79  	dispatcher := yarpc.NewDispatcher(yarpc.Config{
    80  		Name: "client",
    81  		Outbounds: yarpc.Outbounds{
    82  			"service": transport.Outbounds{
    83  				ServiceName: "service",
    84  				Unary:       ob,
    85  			},
    86  		},
    87  	})
    88  	require.NoError(t, dispatcher.Start(), "start client dispatcher")
    89  	rawClient := raw.New(dispatcher.ClientConfig("service"))
    90  	return dispatcher, rawClient
    91  }
    92  
    93  // NewServer creates an echo server using the given inbound from any transport.
    94  func (s TransportSpec) NewServer(t *testing.T, addr string) (*yarpc.Dispatcher, string) {
    95  	xport := s.NewServerTransport(t, addr)
    96  	inbound := s.NewInbound(xport, addr)
    97  
    98  	dispatcher := yarpc.NewDispatcher(yarpc.Config{
    99  		Name:     "service",
   100  		Inbounds: yarpc.Inbounds{inbound},
   101  	})
   102  	Register(dispatcher)
   103  
   104  	require.NoError(t, dispatcher.Start(), "start server dispatcher")
   105  
   106  	return dispatcher, s.Addr(xport, inbound)
   107  }
   108  
   109  // TestConnectAndStopRoundRobin is a test that any transport can apply to
   110  // exercise a transport dropping connections if the transport is stopped before
   111  // a pending request can complete.
   112  func (s TransportSpec) TestConnectAndStopRoundRobin(t *testing.T) {
   113  	addr := _unconnectableAddr
   114  
   115  	client, rawClient := s.NewClient(t, []string{addr})
   116  
   117  	done := make(chan struct{})
   118  	go func() {
   119  		defer close(done)
   120  		ctx := context.Background()
   121  		ctx, cancel := context.WithTimeout(ctx, 50*testtime.Millisecond)
   122  		defer cancel()
   123  		assert.Error(t, Call(ctx, rawClient))
   124  	}()
   125  
   126  	time.Sleep(10 * testtime.Millisecond)
   127  	assert.NoError(t, client.Stop())
   128  
   129  	<-done
   130  }
   131  
   132  // TestConcurrentClientsRoundRobin is a reusable test that any transport can
   133  // apply to cover connection reuse.
   134  func (s TransportSpec) TestConcurrentClientsRoundRobin(t *testing.T) {
   135  	var wg sync.WaitGroup
   136  	count := _concurrentAttempts
   137  
   138  	server, addr := s.NewServer(t, "127.0.0.1:0")
   139  	defer server.Stop()
   140  
   141  	client, rawClient := s.NewClient(t, []string{addr})
   142  	defer client.Stop()
   143  
   144  	wg.Add(count)
   145  	call := func() {
   146  		defer wg.Done()
   147  		ctx := context.Background()
   148  		ctx, cancel := context.WithTimeout(ctx, 150*testtime.Millisecond)
   149  		defer cancel()
   150  		assert.NoError(t, Call(ctx, rawClient))
   151  	}
   152  	for i := 0; i < count; i++ {
   153  		go call()
   154  		time.Sleep(10 * testtime.Millisecond)
   155  	}
   156  
   157  	wg.Wait()
   158  }
   159  
   160  // TestBackoffConnRoundRobin is a reusable test that any transport can apply to
   161  // cover connection management backoff.
   162  func (s TransportSpec) TestBackoffConnRoundRobin(t *testing.T) {
   163  	conn, err := net.Listen("tcp", "127.0.0.1:0")
   164  	require.NoError(t, err)
   165  	addr := conn.Addr().String()
   166  	conn.Close()
   167  
   168  	done := make(chan struct{})
   169  	go func() {
   170  		defer close(done)
   171  
   172  		client, rawClient := s.NewClient(t, []string{addr})
   173  		defer client.Stop()
   174  
   175  		ctx := context.Background()
   176  		ctx, cancel := context.WithTimeout(ctx, testtime.Second)
   177  		defer cancel()
   178  
   179  		// Eventually succeeds, when the server comes online.
   180  		assert.NoError(t, Call(ctx, rawClient))
   181  	}()
   182  
   183  	// Give the client time to make multiple connection attempts.
   184  	time.Sleep(10 * testtime.Millisecond)
   185  	server, _ := s.NewServer(t, addr)
   186  	defer server.Stop()
   187  
   188  	<-done
   189  }
   190  
   191  // Blast sends a blast of calls to the client and verifies that they do not
   192  // err.
   193  func Blast(ctx context.Context, t *testing.T, rawClient raw.Client) {
   194  	for i := 0; i < 10; i++ {
   195  		assert.NoError(t, Call(ctx, rawClient))
   196  	}
   197  }
   198  
   199  // CallUntilSuccess sends a request until it succeeds.
   200  func CallUntilSuccess(t *testing.T, rawClient raw.Client, interval time.Duration) {
   201  	for i := 0; i < _maxAttempts; i++ {
   202  		ctx := context.Background()
   203  		ctx, cancel := context.WithTimeout(ctx, interval)
   204  		err := Call(ctx, rawClient)
   205  		cancel()
   206  		if err == nil {
   207  			return
   208  		}
   209  	}
   210  	assert.Fail(t, "call until success failed multiple times")
   211  }
   212  
   213  // Call sends an echo request to the client.
   214  func Call(ctx context.Context, rawClient raw.Client) error {
   215  	ctx, cancel := context.WithTimeout(ctx, 100*testtime.Millisecond)
   216  	defer cancel()
   217  	res, err := rawClient.Call(ctx, "echo", []byte("hello"))
   218  	if err != nil {
   219  		return err
   220  	}
   221  	if !bytes.Equal(res, []byte("hello")) {
   222  		return fmt.Errorf("unexpected response %+v", res)
   223  	}
   224  	return nil
   225  }
   226  
   227  // Timeout sends a request to the client, which will timeout on the server.
   228  func Timeout(ctx context.Context, rawClient raw.Client) error {
   229  	_, err := rawClient.Call(ctx, "timeout", []byte{})
   230  	return err
   231  }
   232  
   233  // Register registers an echo procedure handler on a dispatcher.
   234  func Register(dispatcher *yarpc.Dispatcher) {
   235  	dispatcher.Register(raw.Procedure("echo", func(ctx context.Context, req []byte) ([]byte, error) {
   236  		return req, nil
   237  	}))
   238  	dispatcher.Register(raw.Procedure("timeout", func(ctx context.Context, req []byte) ([]byte, error) {
   239  		<-ctx.Done()
   240  		return nil, context.DeadlineExceeded
   241  	}))
   242  }