-
Notifications
You must be signed in to change notification settings - Fork 4.5k
grpc: Fix cardinality violations in non-client streaming RPCs. #8385
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
base: master
Are you sure you want to change the base?
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #8385 +/- ##
==========================================
+ Coverage 82.44% 82.48% +0.03%
==========================================
Files 413 413
Lines 40424 40532 +108
==========================================
+ Hits 33328 33431 +103
Misses 5742 5742
- Partials 1354 1359 +5
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it possible to implement all of this in server.go
to avoid adding state to the serverStream
?
As discussed this with @arjan-bal offline, Cardinality violations can only be detected when messages are being read from the stream. This reading process occurs specifically within the server.RecvMsg() function. Since RecvMsg() is invoked from the user-implemented handler, it's not possible to detect cardinality violations during the initial stream setup phase. |
stream.go
Outdated
} else if err != nil { | ||
return err | ||
} | ||
return status.Error(codes.Internal, "cardinality violation: expected <EOF> for non client-streaming RPCs, but received another message") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These error messages go out on the wire. To clients written in any language. So they should generically explain the problem and not use Go-specific terminology, or internal implementation details like "expected EOF".
That's probably not appropriate on the client side, either, but slightly less bad. But since you're changing things, that one should probably look like:
return status.Errorf(codes.Internal, "cardinality violation: received multiple response messages for non-server-streaming RPC")
stream.go
Outdated
} else if err != nil { | ||
return err | ||
} | ||
return status.Error(codes.Internal, "cardinality violation: expected <EOF> for non client-streaming RPCs, but received another message") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see it.
// Second call to SendMsg should fail with Internal error and result in closing | ||
// the connection with a RST_STREAM. | ||
func (s) TestServerStreaming_ClientCallSendMsgTwice(t *testing.T) { | ||
// To ensure server.recvMsg() is successfully completed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this actually needed? Why? If the client were to attempt to send its second message immediately, and that caused a RST_STREAM, that would all happen after the server processed the headers, and the handler should get invoked regardless, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The first call to server.RecvMsg is made by the generated handler before the test's server handler is invoked. If this call fails, the server handler in the test will not be executed at all.
To handle this, I synchronise the calls in the following way:
- Client.SendMsg()
- Server.RecvMsg()
- Client.SendMsg() – Expected to fail with an Internal error and close connection with RST_STREAM.
- Server.SendMsg() – Expected to fail with a Canceled error
An alternative approach would be to use a fake server implementation, which would allow us to directly observe and assert the error returned from server.RecvMsg().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How does that help? The first Server.RecvMsg
should be hung waiting for the client to send an END_STREAM, shouldn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it will not hung.
For non-client-streaming RPCs, initial call to client.SendMsg
will close the client stream with EOF.
Line 1102 in ac13172
if err := a.transportStream.Write(hdr, payld, &transport.WriteOptions{Last: !cs.desc.ClientStreams}); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh yes, since the client knows it's not client-streaming, too, then it will do END_STREAM along with the first call to SendMsg.
So then the reason for the synchronization here is that, if the client does a RST_STREAM, that might propagate to the server and cause its first recv to fail -- even though the first request message was received perfectly normally -- because we cancel the stream's context upon RST_STREAM receipt?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, correct.
So, should we keep the test as it is or change it with alternative approach?
An alternative approach would be to use a fake server implementation, which would allow us to directly observe and assert the error returned from server.RecvMsg().
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, but can you explain more like how I did, to say why the synchronization is needed? Because there would be a race between client cancellation and the server reading the first request message.
desc := &grpc.StreamDesc{ | ||
StreamName: "StreamingOutputCall", | ||
ServerStreams: true, | ||
ClientStreams: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like we should have a test case that sets this true
, also, and ensures that the server errors? This is theoretically catching a client-side check that errors if zero requests are sent before CloseSend
is called when the client knows it is not client-streaming.
(And if we don't have such a check we may want to add one.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to clarify — you're suggesting me to add a test where the client behaves as client/bidi-streaming and sends zero request, while server behave as server-streaming, and then assert that it fails on the server side due to a cardinality violation. Is that correct?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. This exact test, pretty much, but set this field to true
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have modified the test to a table-driven test, where server will run against multiple streamdesc including client-streaming, server-streaming and bidi-streaming.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The key difference between the two cases that I'd like to see is whether the client knows it's required to send a message.
In the case where the client knows (ClientStreams: false
), we should detect the error locally and send a RST_STREAM to the server, but if the client doesn't know (ClientStreams: true
), the server should detect the error and end the stream with an INTERNAL error. Can we confirm these things are happening? (And AFACT the test will fail since the client doesn't check whether it has sent a message in CloseSend
, so it's fine to make that test case Skip
until we fix it in another PR.)
stream.go
Outdated
@@ -1820,7 +1820,7 @@ func (ss *serverStream) RecvMsg(m any) (err error) { | |||
} else if err != nil { | |||
return err | |||
} | |||
return status.Error(codes.Internal, "cardinality violation: expected <EOF> for non client-streaming RPCs, but received another message") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-client-streaming was right before, wasn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, didn't realise while copying it.
Made the changes.
desc := &grpc.StreamDesc{ | ||
StreamName: "StreamingOutputCall", | ||
ServerStreams: true, | ||
ClientStreams: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Correct. This exact test, pretty much, but set this field to true
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM except the few tiny things. Thanks!
|
||
// Tests the behavior for unary RPC when client calls SendMsg twice. Second call | ||
// to SendMsg should fail with Internal error. | ||
func (s) TestUnaryRPC_ClientCallSendMsgTwice(t *testing.T) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is testing client-side checks. We should also have an equivalent test for server-side checks of the same condition (which does not exist). We do have tests for logic on the server for server-streaming RPCs, but not for unary. Unary handling goes through a different code path.
We can add that as a follow-on PR.
// Second call to SendMsg should fail with Internal error and result in closing | ||
// the connection with a RST_STREAM. | ||
func (s) TestServerStreaming_ClientCallSendMsgTwice(t *testing.T) { | ||
// To ensure server.recvMsg() is successfully completed. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, but can you explain more like how I did, to say why the synchronization is needed? Because there would be a race between client cancellation and the server reading the first request message.
|
||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout) | ||
defer cancel() | ||
cc, err := grpc.NewClient(ss.Address, grpc.WithTransportCredentials(insecure.NewCredentials())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you switch all these tests to use local credentials instead of insecure?
|
||
// Tests the behavior of client for server-side streaming RPC when client sends zero request messages. | ||
func (s) TestServerStreaming_ClientSendsZeroRequests(t *testing.T) { | ||
t.Skip() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a string here and include the issue number (#7286)
Partially addresses: #7286
In non-client streaming RPCs, the client's SendMsg() method is designed to automatically close the send operation after its initial call. If someone attempts to call Client.SendMsg() twice for non-client streaming RPCs, if will return with error
Internal desc = SendMsg called after CloseSend
.To mirror this behavior, the server-side logic has been updated so that calling RecvMsg() more than once for non-client streaming RPCs will now similarly return an
Internal
error.RELEASE NOTES: