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 }