mirror of https://github.com/grpc/grpc-go.git
				
				
				
			test: fix raceyness check to deflake test http server (#5866)
Fixes https://github.com/grpc/grpc-go/issues/4990
This commit is contained in:
		
							parent
							
								
									54b7d03e0f
								
							
						
					
					
						commit
						b2d4d5dbae
					
				|  | @ -888,6 +888,32 @@ type lazyConn struct { | |||
| 	beLazy int32 | ||||
| } | ||||
| 
 | ||||
| // possible conn closed errors.
 | ||||
| const possibleConnResetMsg = "connection reset by peer" | ||||
| const possibleEOFMsg = "error reading from server: EOF" | ||||
| 
 | ||||
| // isConnClosedErr checks the error msg for possible conn closed messages. There
 | ||||
| // is a raceyness in the timing of when TCP packets are sent from client to
 | ||||
| // server, and when we tell the server to stop, so we need to check for both of
 | ||||
| // these possible error messages:
 | ||||
| //  1. If the call to ss.S.Stop() causes the server's sockets to close while
 | ||||
| //     there's still in-fight data from the client on the TCP connection, then
 | ||||
| //     the kernel can send an RST back to the client (also see
 | ||||
| //     https://stackoverflow.com/questions/33053507/econnreset-in-send-linux-c).
 | ||||
| //     Note that while this condition is expected to be rare due to the
 | ||||
| //     test httpServer start synchronization, in theory it should be possible,
 | ||||
| //     e.g. if the client sends a BDP ping at the right time.
 | ||||
| //  2. If, for example, the call to ss.S.Stop() happens after the RPC headers
 | ||||
| //     have been received at the server, then the TCP connection can shutdown
 | ||||
| //     gracefully when the server's socket closes.
 | ||||
| //  3. If there is an actual io.EOF received because the client stopped the stream.
 | ||||
| func isConnClosedErr(err error) bool { | ||||
| 	errContainsConnResetMsg := strings.Contains(err.Error(), possibleConnResetMsg) | ||||
| 	errContainsEOFMsg := strings.Contains(err.Error(), possibleEOFMsg) | ||||
| 
 | ||||
| 	return errContainsConnResetMsg || errContainsEOFMsg || err == io.EOF | ||||
| } | ||||
| 
 | ||||
| func (l *lazyConn) Write(b []byte) (int, error) { | ||||
| 	if atomic.LoadInt32(&(l.beLazy)) == 1 { | ||||
| 		time.Sleep(time.Second) | ||||
|  | @ -1013,18 +1039,7 @@ func (s) TestDetailedConnectionCloseErrorPropagatesToRpcError(t *testing.T) { | |||
| 
 | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) | ||||
| 	defer cancel() | ||||
| 	// The precise behavior of this test is subject to raceyness around the timing of when TCP packets
 | ||||
| 	// are sent from client to server, and when we tell the server to stop, so we need to account for both
 | ||||
| 	// of these possible error messages:
 | ||||
| 	// 1) If the call to ss.S.Stop() causes the server's sockets to close while there's still in-fight
 | ||||
| 	//    data from the client on the TCP connection, then the kernel can send an RST back to the client (also
 | ||||
| 	//    see https://stackoverflow.com/questions/33053507/econnreset-in-send-linux-c). Note that while this
 | ||||
| 	//    condition is expected to be rare due to the rpcStartedOnServer synchronization, in theory it should
 | ||||
| 	//    be possible, e.g. if the client sends a BDP ping at the right time.
 | ||||
| 	// 2) If, for example, the call to ss.S.Stop() happens after the RPC headers have been received at the
 | ||||
| 	//    server, then the TCP connection can shutdown gracefully when the server's socket closes.
 | ||||
| 	const possibleConnResetMsg = "connection reset by peer" | ||||
| 	const possibleEOFMsg = "error reading from server: EOF" | ||||
| 
 | ||||
| 	// Start an RPC. Then, while the RPC is still being accepted or handled at the server, abruptly
 | ||||
| 	// stop the server, killing the connection. The RPC error message should include details about the specific
 | ||||
| 	// connection error that was encountered.
 | ||||
|  | @ -1037,7 +1052,10 @@ func (s) TestDetailedConnectionCloseErrorPropagatesToRpcError(t *testing.T) { | |||
| 	// the RPC has been started on it.
 | ||||
| 	<-rpcStartedOnServer | ||||
| 	ss.S.Stop() | ||||
| 	if _, err := stream.Recv(); err == nil || (!strings.Contains(err.Error(), possibleConnResetMsg) && !strings.Contains(err.Error(), possibleEOFMsg)) { | ||||
| 	// The precise behavior of this test is subject to raceyness around the timing
 | ||||
| 	// of when TCP packets are sent from client to server, and when we tell the
 | ||||
| 	// server to stop, so we need to account for both possible error messages.
 | ||||
| 	if _, err := stream.Recv(); err == io.EOF || !isConnClosedErr(err) { | ||||
| 		t.Fatalf("%v.Recv() = _, %v, want _, rpc error containing substring: %q OR %q", stream, err, possibleConnResetMsg, possibleEOFMsg) | ||||
| 	} | ||||
| 	close(rpcDoneOnClient) | ||||
|  | @ -6739,31 +6757,37 @@ func (s) TestRPCWaitsForResolver(t *testing.T) { | |||
| func (s) TestHTTPHeaderFrameErrorHandlingHTTPMode(t *testing.T) { | ||||
| 	// Non-gRPC content-type fallback path.
 | ||||
| 	for httpCode := range transport.HTTPStatusConvTab { | ||||
| 		doHTTPHeaderTest(t, transport.HTTPStatusConvTab[int(httpCode)], []string{ | ||||
| 		if err := doHTTPHeaderTest(t, transport.HTTPStatusConvTab[int(httpCode)], []string{ | ||||
| 			":status", fmt.Sprintf("%d", httpCode), | ||||
| 			"content-type", "text/html", // non-gRPC content type to switch to HTTP mode.
 | ||||
| 			"grpc-status", "1", // Make up a gRPC status error
 | ||||
| 			"grpc-status-details-bin", "???", // Make up a gRPC field parsing error
 | ||||
| 		}) | ||||
| 		}); err != nil { | ||||
| 			t.Error(err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Missing content-type fallback path.
 | ||||
| 	for httpCode := range transport.HTTPStatusConvTab { | ||||
| 		doHTTPHeaderTest(t, transport.HTTPStatusConvTab[int(httpCode)], []string{ | ||||
| 		if err := doHTTPHeaderTest(t, transport.HTTPStatusConvTab[int(httpCode)], []string{ | ||||
| 			":status", fmt.Sprintf("%d", httpCode), | ||||
| 			// Omitting content type to switch to HTTP mode.
 | ||||
| 			"grpc-status", "1", // Make up a gRPC status error
 | ||||
| 			"grpc-status-details-bin", "???", // Make up a gRPC field parsing error
 | ||||
| 		}) | ||||
| 		}); err != nil { | ||||
| 			t.Error(err) | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	// Malformed HTTP status when fallback.
 | ||||
| 	doHTTPHeaderTest(t, codes.Internal, []string{ | ||||
| 	if err := doHTTPHeaderTest(t, codes.Internal, []string{ | ||||
| 		":status", "abc", | ||||
| 		// Omitting content type to switch to HTTP mode.
 | ||||
| 		"grpc-status", "1", // Make up a gRPC status error
 | ||||
| 		"grpc-status-details-bin", "???", // Make up a gRPC field parsing error
 | ||||
| 	}) | ||||
| 	}); err != nil { | ||||
| 		t.Error(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Testing erroneous ResponseHeader or Trailers-only (delivered in the first HEADERS frame).
 | ||||
|  | @ -6809,18 +6833,22 @@ func (s) TestHTTPHeaderFrameErrorHandlingInitialHeader(t *testing.T) { | |||
| 			errCode: codes.Unavailable, | ||||
| 		}, | ||||
| 	} { | ||||
| 		doHTTPHeaderTest(t, test.errCode, test.header) | ||||
| 		if err := doHTTPHeaderTest(t, test.errCode, test.header); err != nil { | ||||
| 			t.Error(err) | ||||
| 		} | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| // Testing non-Trailers-only Trailers (delivered in second HEADERS frame)
 | ||||
| func (s) TestHTTPHeaderFrameErrorHandlingNormalTrailer(t *testing.T) { | ||||
| 	for _, test := range []struct { | ||||
| 	tests := []struct { | ||||
| 		name           string | ||||
| 		responseHeader []string | ||||
| 		trailer        []string | ||||
| 		errCode        codes.Code | ||||
| 	}{ | ||||
| 		{ | ||||
| 			name: "trailer missing grpc-status", | ||||
| 			responseHeader: []string{ | ||||
| 				":status", "200", | ||||
| 				"content-type", "application/grpc", | ||||
|  | @ -6832,6 +6860,7 @@ func (s) TestHTTPHeaderFrameErrorHandlingNormalTrailer(t *testing.T) { | |||
| 			errCode: codes.Unavailable, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "malformed grpc-status-details-bin field with status 404", | ||||
| 			responseHeader: []string{ | ||||
| 				":status", "404", | ||||
| 				"content-type", "application/grpc", | ||||
|  | @ -6844,6 +6873,7 @@ func (s) TestHTTPHeaderFrameErrorHandlingNormalTrailer(t *testing.T) { | |||
| 			errCode: codes.Unimplemented, | ||||
| 		}, | ||||
| 		{ | ||||
| 			name: "malformed grpc-status-details-bin field with status 200", | ||||
| 			responseHeader: []string{ | ||||
| 				":status", "200", | ||||
| 				"content-type", "application/grpc", | ||||
|  | @ -6855,8 +6885,14 @@ func (s) TestHTTPHeaderFrameErrorHandlingNormalTrailer(t *testing.T) { | |||
| 			}, | ||||
| 			errCode: codes.Internal, | ||||
| 		}, | ||||
| 	} { | ||||
| 		doHTTPHeaderTest(t, test.errCode, test.responseHeader, test.trailer) | ||||
| 	} | ||||
| 	for _, test := range tests { | ||||
| 		t.Run(test.name, func(t *testing.T) { | ||||
| 			if err := doHTTPHeaderTest(t, test.errCode, test.responseHeader, test.trailer); err != nil { | ||||
| 				t.Error(err) | ||||
| 			} | ||||
| 		}) | ||||
| 
 | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  | @ -6865,7 +6901,9 @@ func (s) TestHTTPHeaderFrameErrorHandlingMoreThanTwoHeaders(t *testing.T) { | |||
| 		":status", "200", | ||||
| 		"content-type", "application/grpc", | ||||
| 	} | ||||
| 	doHTTPHeaderTest(t, codes.Internal, header, header, header) | ||||
| 	if err := doHTTPHeaderTest(t, codes.Internal, header, header, header); err != nil { | ||||
| 		t.Fatal(err) | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
| type httpServerResponse struct { | ||||
|  | @ -6930,14 +6968,14 @@ func (s *httpServer) start(t *testing.T, lis net.Listener) { | |||
| 		} | ||||
| 		writer.Flush() // necessary since client is expecting preface before declaring connection fully setup.
 | ||||
| 		var sid uint32 | ||||
| 		// Loop until conn is closed and framer returns io.EOF
 | ||||
| 		// Loop until framer returns possible conn closed errors.
 | ||||
| 		for requestNum := 0; ; requestNum = (requestNum + 1) % len(s.responses) { | ||||
| 			// Read frames until a header is received.
 | ||||
| 			for { | ||||
| 				frame, err := framer.ReadFrame() | ||||
| 				if err != nil { | ||||
| 					if err != io.EOF { | ||||
| 						t.Errorf("Error at server-side while reading frame. Err: %v", err) | ||||
| 					if !isConnClosedErr(err) { | ||||
| 						t.Errorf("Error at server-side while reading frame. got: %q, want: rpc error containing substring %q OR %q", err, possibleConnResetMsg, possibleEOFMsg) | ||||
| 					} | ||||
| 					return | ||||
| 				} | ||||
|  | @ -6994,11 +7032,10 @@ func (s *httpServer) start(t *testing.T, lis net.Listener) { | |||
| 	}() | ||||
| } | ||||
| 
 | ||||
| func doHTTPHeaderTest(t *testing.T, errCode codes.Code, headerFields ...[]string) { | ||||
| 	t.Helper() | ||||
| func doHTTPHeaderTest(t *testing.T, errCode codes.Code, headerFields ...[]string) error { | ||||
| 	lis, err := net.Listen("tcp", "localhost:0") | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("Failed to listen. Err: %v", err) | ||||
| 		return fmt.Errorf("listening on %q: %v", "localhost:0", err) | ||||
| 	} | ||||
| 	defer lis.Close() | ||||
| 	server := &httpServer{ | ||||
|  | @ -7007,7 +7044,7 @@ func doHTTPHeaderTest(t *testing.T, errCode codes.Code, headerFields ...[]string | |||
| 	server.start(t, lis) | ||||
| 	cc, err := grpc.Dial(lis.Addr().String(), grpc.WithTransportCredentials(insecure.NewCredentials())) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("failed to dial due to err: %v", err) | ||||
| 		return fmt.Errorf("dial(%q): %v", lis.Addr().String(), err) | ||||
| 	} | ||||
| 	defer cc.Close() | ||||
| 	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) | ||||
|  | @ -7015,11 +7052,12 @@ func doHTTPHeaderTest(t *testing.T, errCode codes.Code, headerFields ...[]string | |||
| 	client := testpb.NewTestServiceClient(cc) | ||||
| 	stream, err := client.FullDuplexCall(ctx) | ||||
| 	if err != nil { | ||||
| 		t.Fatalf("error creating stream due to err: %v", err) | ||||
| 		return fmt.Errorf("creating FullDuplex stream: %v", err) | ||||
| 	} | ||||
| 	if _, err := stream.Recv(); err == nil || status.Code(err) != errCode { | ||||
| 		t.Fatalf("stream.Recv() = _, %v, want error code: %v", err, errCode) | ||||
| 		return fmt.Errorf("stream.Recv() = %v, want error code: %v", err, errCode) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func (s) TestClientCancellationPropagatesUnary(t *testing.T) { | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue