-
Notifications
You must be signed in to change notification settings - Fork 4.5k
grpc: Fix cardinality violations in client streaming RPCs #8278
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 ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #8278 +/- ##
==========================================
- Coverage 82.33% 82.20% -0.14%
==========================================
Files 419 419
Lines 42062 42068 +6
==========================================
- Hits 34631 34580 -51
- Misses 5979 6019 +40
- Partials 1452 1469 +17
🚀 New features to boost your workflow:
|
stream.go
Outdated
return io.EOF // indicates successful end of stream. | ||
} | ||
|
||
return toRPCErr(err) | ||
} | ||
if cs.desc.ClientStreams { | ||
cs.recvMsg = 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.
Can we set this boolean without the check? Otherwise we would need to document the variable in a way to make it obvious that this is set only for client streaming RPCs. Even then users may accidentally use it for other types of RPCs.
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.
Changed it to !cs.desc.ServerStreams
, so that it will mark cs.recvFirstMsg=true
for both unary and clientStreaming RPCs.
Do we need a similar check on the server side also, i.e. change the status to internal if the server attempts to close without writing a message? |
stream.go
Outdated
@@ -1134,11 +1136,17 @@ func (a *csAttempt) recvMsg(m any, payInfo *payloadInfo) (err error) { | |||
if statusErr := a.transportStream.Status().Err(); statusErr != nil { | |||
return statusErr | |||
} | |||
if !cs.desc.ServerStreams && !cs.recvFirstMsg { | |||
return status.Errorf(codes.Internal, "client streaming cardinality violation") |
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.
Let's be more specific about what happened here so that a user could understand what's happening when they see the error.
"received no response message from non-streaming RPC"
?
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.
Sure.
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 a change here.
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.
@Pranjali-2501 bump.
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.
Done.
stream.go
Outdated
return io.EOF // indicates successful end of stream. | ||
} | ||
|
||
return toRPCErr(err) | ||
} | ||
if !cs.desc.ServerStreams { | ||
cs.recvFirstMsg = 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.
Should there also be something here that fails if recvFirstMsg
is already set? (That would indicate we received multiple messages in a unary RPC.)
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.
For both unary and client-streaming RPCs, client.recvmsg() will wait for the trailers after receiving its first msg over here .
I'll change the status code here from UNKNOWN
to INTERNAL
in upcoming PR with tests. It will handle the case when trailers are not send from server side for unary and client-streaming RPCs.
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 see. So why do you need this new field, then?
If the above recv
ever returns io.EOF
, then we already know there was never a message read from the stream. Either that, or the user called Recv
after receiving an error from a previous call to Recv
, which is illegal and undefined.
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.
Yeah, make sense.
So for non-server streaming rpcs, we can directly return INTERNAL
error if the first recv() call gets EOF.
if !cs.desc.ServerStreams {
return status.Errorf(codes.Internal, "client streaming cardinality violation")
}
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, I think that should work and avoids adding new state to track.
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.
Ping - I think we can remove recvFirstMsg
now?
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 had reverted that in this commit bbd8366.
But than @arjan-bal pointed out that in case of successful RPC, if user call RecvMsg() twice, the second RecvMsg() call should return with io.EOF instead of cardinality violation.
That's why I reverted back to the older code to fulfill that condition.
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.
As I mentioned before, it's not defined what happens if RecvMsg
is called after returning an error.
There are other scenarios where calling recvMsg
repeatedly will not cause the previously returned error to occur. E.g. if a received message is too large, recv
will return an error. But I believe a subsequent call to RecvMsg
would actually return the next message from the stream, or io.EOF
at the end of the stream.
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.
Acknowledged, if we're fine with changing the existing behaviour of returning io.EOF
on subsequent calls after an RPC ends successfully, we can get rid of the extra variable to track state.
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 removed the extra variable and the tests that were added to check the scenario when recvMsg() called twice.
stream.go
Outdated
@@ -1134,11 +1136,17 @@ func (a *csAttempt) recvMsg(m any, payInfo *payloadInfo) (err error) { | |||
if statusErr := a.transportStream.Status().Err(); statusErr != nil { | |||
return statusErr | |||
} | |||
if !cs.desc.ServerStreams && !cs.recvFirstMsg { | |||
return status.Errorf(codes.Internal, "client streaming cardinality violation") |
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 a change here.
test/end2end_test.go
Outdated
return | ||
} | ||
defer conn.Close() | ||
framer := http2.NewFramer(conn, conn) |
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.
Realistically this test shouldn't need low level framer access. You should be able to implement it with a grpc.Server
that has its EmptyCall
handler configured as a streaming handling, and with an implementation that just returns nil
immediately without sending a message.
I do think that would be preferable, because the framing stuff is tedious and error prone, and it makes the test much more complex. If you're having too much trouble with that approach, let me know and I can try to help, or if it somehow turns out to be a lot more work than I was expecting, feel free to push back.
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.
Initially, I attempted to resolve this by returning nil from EmptyCall. However, this approach resulted in an empty, successful response rather than a failure, which wasn't the desired outcome.
The scenario that sends no response cannot be replicated within grpc-go itself, as the framework requires a response to be sent.
This test aims to verify behavior in a cross-language context, where a unary client might interact with a unary server implemented in a different language, potentially allowing the server to send no response.
Please let me know if that is not the case or I overlook something.
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 think @dfawley is saying that we can register a streaming handler for a unary method on the server. The client will call EmptyCall
, but the server will call a streaming handler.
srv := grpc.NewServer()
serviceDesc := grpc.ServiceDesc{
ServiceName: "grpc.testing.TestService",
HandlerType: (*any)(nil),
Methods: []grpc.MethodDesc{},
Streams: []grpc.StreamDesc{
{
StreamName: "EmptyCall",
Handler: func(any, grpc.ServerStream) error {
return nil
},
ClientStreams: true,
},
},
Metadata: "grpc/testing/test.proto",
}
srv.RegisterService(&serviceDesc, &testServer{})
go srv.Serve(lis)
defer srv.Stop()
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.
Thanks for clarifying. I had made the changes.
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.
Another option (maybe easier, but no need to change now) is to use the unknown service handler, and don't register anything on the server: https://pkg.go.dev/google.golang.org/grpc#UnknownServiceHandler
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.
UnknownServiceHandler
uses bidirectional streaming[desc.clientstreams:true
, desc.serverstreams:true
].
Will it work with the changes I made to addresses cardinality violations?
if !cs.desc.ServerStreams && !cs.recvFirstMsg {
return status.Errorf(codes.Internal, "cardinality violation: received no response message from non-streaming RPC")
}
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 idea is to make the server think it's a bidi stream, but the client thinks it's not. The client's setting is what the code snippet you are showing is checking. The client has no knowledge of how the server is treating the stream.
@@ -1134,6 +1134,10 @@ func (a *csAttempt) recvMsg(m any, payInfo *payloadInfo) (err error) { | |||
if statusErr := a.transportStream.Status().Err(); statusErr != nil { | |||
return statusErr | |||
} | |||
// received no msg and status ok for non-server streaming rpcs. | |||
if !cs.desc.ServerStreams { |
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.
There is a similar method on addrConnStream that also needs to be updated.
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'll made the similar changes once the changes made in client.RecvMsg() gets finalized.
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.
Done.
Partially addresses: #7286
Client should ensure an "Internal" error is returned for client-streaming RPCs if the server doesn't send a message before returning status OK.
Currently it is returning "EOF", which should not be the case for cardinality violations.
RELEASE NOTES: