xds/extproc: adds ClientInterceptor and ClientStream implementation for normal mode for gRFC A93#9174
xds/extproc: adds ClientInterceptor and ClientStream implementation for normal mode for gRFC A93#9174eshitachandwani wants to merge 36 commits into
Conversation
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #9174 +/- ##
==========================================
- Coverage 83.30% 83.00% -0.31%
==========================================
Files 421 422 +1
Lines 34061 34832 +771
==========================================
+ Hits 28375 28911 +536
- Misses 4261 4425 +164
- Partials 1425 1496 +71
🚀 New features to boost your workflow:
|
….Done case becuase also cancel context incase of error to avoid calls to dataplane stream hanging.
| req := cs.newProcessingRequest(true) | ||
| req.Request = &v3procservicepb.ProcessingRequest_RequestBody{ | ||
| RequestBody: &v3procservicepb.HttpBody{ | ||
| EndOfStream: true, |
There was a problem hiding this comment.
In the "Messages Sent To the ext_proc Server" section of the gRFC, this field is documented as follows:
end_of_stream: For client messages, may be true if the client sent a half-close at the same time as the last message. For server messages, will always be false.
Given the above, shouldn't this be set to false (i.e., not set at all) because our CloseSend does not provide a way to half-close with a message at the same time.
There was a problem hiding this comment.
That is what I had earlier , but then the gRFC changed to say that for response , we will consider EOS without message only if EOS is also true , so I thought that should be true for request too.
end_of_stream_without_message : Will be set to true to indicate a half-close with no message to send. Ignored if end_of_stream is false.
And I have a comment on the gRFC to clarify the same : grpc/proposal#484 (comment)
Let me know what you think.
There was a problem hiding this comment.
Can we ping that comment thread on the gRFC please?
| cs.failProcStream(fmt.Errorf("extproc: external processor returned invalid status instead of CONTINUE for response headers")) | ||
| return | ||
| } | ||
| if err = cs.config.mutationRules.ApplyAdditions(header.GetResponse().GetHeaderMutation().GetSetHeaders(), cs.responseHeader); err != nil { |
| // type to send the data to the dataplane server. | ||
| if !cs.reqForwardingStarted { | ||
| cs.reqForwardingStarted = true | ||
| go cs.requestForwardingToDataplaneLoop(msg.ProtoReflect().Type()) |
There was a problem hiding this comment.
Are we certain that this is always the same type for the life of an RPC?
(Less important): It is possible to get this type from some descriptor to avoid this?
There was a problem hiding this comment.
Are we certain that this is always the same type for the life of an RPC?
I am not sure what you mean, but I dont think it is possible to change the proto mid RPC.
(Less important): It is possible to get this type from some descriptor to avoid this?
I dont think there is an easy or direct way to get this from any descriptor, I dont see this message type being stored anywhere inside grpc go.
There was a problem hiding this comment.
I am not sure what you mean, but I dont think it is possible to change the proto mid RPC.
What I meant only applies when someone is not using code generated using our protoc-gen-go-grpc. Then, they could call SendMsg with different type of proto messages over the life of a stream.
| req := cs.newProcessingRequest(true) | ||
| req.Request = &v3procservicepb.ProcessingRequest_RequestBody{ | ||
| RequestBody: &v3procservicepb.HttpBody{ | ||
| EndOfStream: true, |
There was a problem hiding this comment.
Can we ping that comment thread on the gRFC please?
| if strings.HasSuffix(key, "-bin") { | ||
| rawValue = []byte(base64.StdEncoding.EncodeToString(rawValue)) | ||
| } | ||
| headerMap.Headers = append(headerMap.Headers, &v3corepb.HeaderValue{ | ||
| Key: key, | ||
| RawValue: rawValue, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Couple of simplifications here:
- Make this block into a helper and call it from both here and down below and pass it a
keyof type[]byte - When having to do base64 encoding, use the
base64.StdEncoding.Encodefunction which works on byte slices instead of string and therefore you can avoid one conversion back from a string into a[]byte
| // type to send the data to the dataplane server. | ||
| if !cs.reqForwardingStarted { | ||
| cs.reqForwardingStarted = true | ||
| go cs.requestForwardingToDataplaneLoop(msg.ProtoReflect().Type()) |
There was a problem hiding this comment.
I am not sure what you mean, but I dont think it is possible to change the proto mid RPC.
What I meant only applies when someone is not using code generated using our protoc-gen-go-grpc. Then, they could call SendMsg with different type of proto messages over the life of a stream.
| } | ||
| cs.mutatedReqBuffer.Load() | ||
|
|
||
| if streamedResp.GetEndOfStream() && streamedResp.GetEndOfStreamWithoutMessage() { |
There was a problem hiding this comment.
Can we only check streamedResp.GetEndOfStreamWithoutMessage here? The check for streamedResp.GetEndOfStream down below will catch the case where this entry in the buffer represents a message+EOS.
| } | ||
|
|
||
| // createDataplaneStream initializes the underlying gRPC dataplane stream using | ||
| // the provided newStream function. |
There was a problem hiding this comment.
Nit: Can we document that if dataplane stream creation fails, the top-level context used by the proc filter is cancelled and thereby all spawned goroutines will terminate.
| // Signal that the response header is modified and ready to be sent to the | ||
| // client, so that if there is any buffered response body, it can be sent | ||
| // after the header. | ||
| cs.responseHeaderModified.Fire() |
There was a problem hiding this comment.
By firing this event from here (and similarly for the trailers case below), how are we guaranteeing that ordering is maintained for response messages?
Other response messages to be sent to the client application are added to the mutatedRespBuffer and read from there when the application calls Recv. Shouldn't the headers and trailers go through that buffer as well?
| case <-cs.ctx.Done(): | ||
| return nil | ||
| } | ||
| return cs.responseTrailers |
There was a problem hiding this comment.
Nit: Can we move this return to be under the case <-cs.responseTrailerModified.Done() to be more explicit.
| if cs.trailersOnly { | ||
| return nil, nil | ||
| } | ||
| return cs.responseHeader, nil |
There was a problem hiding this comment.
Nit: Can we move this block to be under the case <-cs.responseHeaderModified.Done() to be more explicit.
| trailers := dataplaneStream.Trailer() | ||
| if len(trailers) > 0 { | ||
| header = trailers | ||
| cs.trailersOnly = true | ||
| } |
There was a problem hiding this comment.
Nit: Possible simplification:
if header == nil {
// A trailers-only response returns nil from dataplaneStream.Header().
headers = dataplaneStream.Trailer()
if len(headers) > 0 {
cs.trailersOnly = true
}
}| } | ||
| cs.mutatedRespBuffer.Put(streamedResp) | ||
|
|
||
| case resp.GetResponseHeaders() != nil: |
There was a problem hiding this comment.
We probably need a little bit more safetey checks here I feel. What if the proc server already sent us a response headers message and we sent cs.responseHeader to the application. Now, the proc server sends one more response headers (clearly a misbehaving proc server), then we end up mutating the same metadata.MD that we've given the client application and that will lead to a data race.
| } | ||
|
|
||
| func (cs *clientStream) RecvMsg(m any) error { | ||
| if cs.procStreamFailed.HasFired() { |
There was a problem hiding this comment.
Based on my other comment where I requested the use of an atomic boolean to guard the inside of initiateResponseHeaderProcessing, can you also check if this condition procStreamFailed can be taken care of by that method. If we can do that (possibly by calling waitForDataplaneStream outside of the boolean guard), then we can directly call initiateResponseHeaderProcessing. Something to consider.
| if val := cs.procStreamErr.Load(); val != nil { | ||
| return val.(error) | ||
| } |
There was a problem hiding this comment.
It is interesting that you use this pattern instead of the more commonly used pattern:
if cs.procStreamFailed.HasFired() {
return cs.procStreamErr.Load().(error)
}Any specific reason for this?
| cs.initiateResponseTrailerProcessing() | ||
| return cs.waitForTrailerProcessing(err) |
There was a problem hiding this comment.
Why are we blocking the application's RecvMsg call on trailers? Shouldn't we return the error we received from the dataplane stream and have them call Trailer if they want to see the trailers?
|
|
||
| // Start the background receiving loop on the first RecvMsg call to capture | ||
| // the type of message to be received. | ||
| if !cs.responseRecvStarted { |
There was a problem hiding this comment.
Nit: should this be called responseForwardingStarted or respForwardingStarted, similar to reqForwardingStarted?
|
|
||
| newMsg := msgType.New().Interface() | ||
| if err := dataplaneStream.RecvMsg(newMsg); err != nil { | ||
| cs.initiateResponseTrailerProcessing() |
There was a problem hiding this comment.
Same concern about why we need to block for trailers here?
| if val := cs.procStreamErr.Load(); val != nil { | ||
| return val.(error) | ||
| } | ||
| s, err := cs.waitForDataplaneStream(cs.ctx) | ||
| if err != nil { | ||
| return err | ||
| } | ||
| if err := s.RecvMsg(m); err != nil { | ||
| cs.initiateResponseTrailerProcessing() | ||
| return cs.waitForTrailerProcessing(err) | ||
| } |
There was a problem hiding this comment.
This block of code is very similar to the one up top where we handle receiving directly from the dataplane stream when some conditions are met. Maybe, we can find a way to share this code?
This PR add implementation of NewStream and ClientStream for normal mode for A93: xds-ext-proc.
This PR does not include channel retention , metrics and observability mode.
#ext-proc-a93
RELEASE NOTES: None