Skip to content

grpc_test: add tests for cardinality violation #8120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 19 commits into from
Apr 25, 2025

Conversation

eshitachandwani
Copy link
Member

@eshitachandwani eshitachandwani commented Feb 25, 2025

Fixes: #7570

Added the following tests for client streaming RPC :

  1. Client should ensure a status internal error is returned for client-streaming RPCs if the server doesn't send a message before returning status OK. This is a cardinality violation. Skipping this test for now till the issues are fixed: Cardinaltiy violation in stream interface implementation #8119
  2. Server can continue to receive messages after calling SendAndClose.
  3. Recv() on client streaming client blocks till the server handler returns even after calling SendAndClose from the server handler
  4. Client receives nil message and non-nil error when server handler returns an error after calling SendAndClose.

RELEASE NOTES: N/A

Copy link

codecov bot commented Feb 25, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 82.19%. Comparing base (ae2a04f) to head (fc205e8).
Report is 82 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8120      +/-   ##
==========================================
- Coverage   82.29%   82.19%   -0.10%     
==========================================
  Files         387      417      +30     
  Lines       38967    41344    +2377     
==========================================
+ Hits        32066    33983    +1917     
- Misses       5586     5939     +353     
- Partials     1315     1422     +107     

see 142 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Comment on lines 148 to 149
earlyNil bool // whether to return nil without calling SendAndClose().
recvAfterClose bool // whether to call Recv() after calling SendAndClose().
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We really really don't want to extend testServer. Could you use the stubserver package instead? testServer has a lot of bad properties (especially: it splits the testing code into two different places: the test and the testServer), and this only complicates it further by adding more ways to conditionalize its behavior.

You can follow this kind of example to use stubserver instead:

ss := &stubserver.StubServer{

Also please add a comment on testServer that it should not be used for any new tests so that this doesn't happen again.

Sorry!

@dfawley dfawley assigned eshitachandwani and unassigned dfawley Feb 27, 2025
Copy link
Contributor

@purnesh42H purnesh42H left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like we need one more test case when "Server Doesn't RST_STREAM"

Server-Side:

  • Create a client-streaming RPC handler.
  • The handler calls SendAndClose with a valid response message.
  • The handler then attempts to call Recv.

Client-Side:

  • Make a client-streaming RPC call.
  • Continuously send messages.

Expected Outcome:

  • The server's Recv call should return an error indicating that the stream is closed.
  • The client's Send calls should eventually return an error indicating that the stream has been reset.

@purnesh42H purnesh42H removed their assignment Mar 4, 2025
@eshitachandwani
Copy link
Member Author

Looks like we need one more test case when "Server Doesn't RST_STREAM"

Server-Side:

  • Create a client-streaming RPC handler.
  • The handler calls SendAndClose with a valid response message.
  • The handler then attempts to call Recv.

Client-Side:

  • Make a client-streaming RPC call.
  • Continuously send messages.

Expected Outcome:

  • The server's Recv call should return an error indicating that the stream is closed.
  • The client's Send calls should eventually return an error indicating that the stream has been reset.

This looks like the test I have written already , the TestClientStreamingRecvAfterCloseError test , can you tell me what is the difference between the two?

@purnesh42H
Copy link
Contributor

Looks like we need one more test case when "Server Doesn't RST_STREAM"
Server-Side:

  • Create a client-streaming RPC handler.
  • The handler calls SendAndClose with a valid response message.
  • The handler then attempts to call Recv.

Client-Side:

  • Make a client-streaming RPC call.
  • Continuously send messages.

Expected Outcome:

  • The server's Recv call should return an error indicating that the stream is closed.
  • The client's Send calls should eventually return an error indicating that the stream has been reset.

This looks like the test I have written already , the TestClientStreamingRecvAfterCloseError test , can you tell me what is the difference between the two?

yeah looks close. I think the for loop is not clear. We should do t.LogF when receiving error from client.Send indicating rst_stream. Is it possible to verify if rst stream was received in trailers. If its too complicated we can skip but let's see if we can. Other thing is the for loop shouldn't be indefinite. We should also do t.Fatalf if context is done. That way its clear that we are expecting an error for this test.

Having said that, then I think then we need a separate test case where client ignores the error after SendAndClose. It will slightly shorter where the handler return a static error and client just ignores it and status is ok.

Copy link
Member Author

@eshitachandwani eshitachandwani left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like I had not published the earlier comments.

Copy link
Contributor

@purnesh42H purnesh42H left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@purnesh42H purnesh42H requested a review from dfawley April 1, 2025 17:15
@purnesh42H purnesh42H assigned dfawley and unassigned purnesh42H Apr 1, 2025
@purnesh42H
Copy link
Contributor

@dfawley for second review

Comment on lines 3589 to 3591
// Tests to verify that client receive a cardinality violation error for
// client-streaming RPCs if the server doesn't send a message before returning
// status OK.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Tests to verify that client receive a cardinality violation error for
// client-streaming RPCs if the server doesn't send a message before returning
// status OK.
// Tests that a client receives a cardinality violation error for
// client-streaming RPCs if the server doesn't send a message before returning
// status OK.

(Please rewrap)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

})
_, err := stream.Recv()
if err == nil {
logger.Fatalf("stream.Recv() = %v, want an error", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You want t.Errorf not logger.Fatalf here. logger.Fatalf kills the whole process. t.Fatalf kills the current goroutine. This won't run in the main test goroutine, so t.Fatalf would just cause problems, not terminate the test.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right!

})
_, err := stream.Recv()
if err == nil {
logger.Fatalf("stream.Recv() = %v, want an error", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "= %v", err -- err is nil. Just put nil in the string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 3623 to 3624
// Test to verify for client-streaming RPCs, when SendAndClose is called, the server
// should reset stream after sending the response message successfully.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't appear to be validating RST_STREAM is written to the connection. You'll need to use the serverTester for that.

Comment on lines 3681 to 3683
if status.Code(err) != codes.OK {
t.Fatalf("stream.CloseAndRecv() = %v, want error %s", err, codes.OK)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

status.Code(nil) is always codes.OK so this doesn't do anything.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.

Comment on lines 3690 to 3692
// TODO : https://github.com/grpc/grpc-go/issues/8119 - remove `t.Skip()`
// after this is fixed.
t.Skip()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the test description above...why does this fail today? I don't think this should be a problem -- I was expecting that it's impossible for the server to do anything else.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure what you mean , because if I send an error code , example Unimplemented , according to the test , it is being reflected in the client when CloseAndRecv() is called by client.
Also in the the code, SendAndCLose() doesnt look like its doing anything to close , just calling SendMsg() . It also says to return nil after calling SendAndCLose() or return an error ,look here so I would think that returning an error after SendAndClose() would surely propogate to client and this is the intended behavior and this test is not needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. As mentioned offline, I'm worried that we won't be able to ever change the behavior of SendAndClose to end the RPC, even though that is what it claims to do. So we should instead confirm that SendAndClose doesn't send the TRAILERS on success. And that returning an error from the handler after SendAndClose results in a TRAILERS frame being sent that contains the error.

How the client handles that may depend on the type of stream from the client's view. E.g. if the client is using it as a bidi streaming RPC, then it should see the error trailers. But if it's a client streaming call, then it might get some other kind of error, since a message and an error is not expected for client streaming RPCs.

sum := 0
in, _ := stream.Recv()
p := in.GetPayload().GetBody()
sum += len(p)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sum := len(p) ? But why is it a sum? Probably this was left over from an old iteration. You can just delete it and return len(p) directly in the response message, too. Or, better, just return 0 in the response. The value is unimportant.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 3717 to 3720
payload, err := newPayload(testpb.PayloadType_COMPRESSABLE, 1)
if err != nil {
t.Fatal(err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems easier to just delete this. Send empty requests instead...

Less code is usually better code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 3733 to 3737
select {
case <-ctx.Done():
t.Fatal("timed out waiting for error from server")
default:
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be replaced by doing:

for ctx.Err() == nil {
	if err := stream.Send()....
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 3729 to 3730
}
if err != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you use an else if here, then you can use err := in the if. That's more natural unless it's important to keep err beyond the loop -- but it isn't.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member

@dfawley dfawley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks! Just a bunch of little things.

t.Skip()
ss := &stubserver.StubServer{
StreamingInputCallF: func(_ testgrpc.TestService_StreamingInputCallServer) error {
return nil
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There should be comments here and there explaining why the test is doing what it is. Like "// Immediately return a success without first sending a response message, which is illegal."

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Comment on lines 3630 to 3632
if resp, err := stream.Recv(); err != nil || resp == nil {
t.Fatalf("stream.Recv() = %v, %v, want non-nil empty response, <nil>", resp, err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you assert that the response matches some expectation, not just "not nil"? I.e. proto.Equal(&testpb.StreamingInputCallRequest{})?

if err := stream.Send(&testpb.StreamingInputCallRequest{}); err != nil {
t.Fatalf("stream.Send(_) = %v, want <nil>", err)
}
if resp, err := stream.CloseAndRecv(); err != nil || resp == nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar here.

Comment on lines 3622 to 3623
// Tests the behavior where Recv() can be called after SendAndClose(). Although
// this is unexpected, we retain it for backward compatibility.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test description shouldn't simply say "tests this scenario", it should say "tests that in this scenario this thing happens". Here it should say something like "Tests that the server can continue to receive messages after calling SendAndClose".

ss := stubserver.StubServer{
StreamingInputCallF: func(stream testgrpc.TestService_StreamingInputCallServer) error {
if err := stream.SendAndClose(&testpb.StreamingInputCallResponse{}); err != nil {
t.Fatalf("stream.SendAndClose(_) = %v, want <nil>", err)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs to be Errorf since it's not in the main test goroutine. Otherwise it just kills the current goroutine, and keeps the test running. (I see the same thing above.)

if err != nil {
t.Fatalf(".StreamingInputCall(_) = _, %v, want <nil>", err)
}
if resp, err := stream.CloseAndRecv(); status.Code(err) != codes.Internal || resp != nil {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You should also check status.Message(err) == status.Message(wantError). And it would be better to use status.Code(wantError) instead of repeating the code in it here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

Copy link
Member

@dfawley dfawley left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, thanks! Just a bunch of little things.

@dfawley dfawley assigned eshitachandwani and unassigned dfawley Apr 24, 2025
@eshitachandwani eshitachandwani merged commit 4cedec4 into grpc:master Apr 25, 2025
15 checks passed
purnesh42H pushed a commit to purnesh42H/grpc-go that referenced this pull request May 4, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Cardinaltiy violation in stream interface implementation Create tests for two potential cardinality violation bugs
4 participants