Skip to content

Commit 5c7f936

Browse files
xds: refactor xDS Client Interceptor API to support CallOption propagation. (#9168)
This PR refactors the client-side xDS HTTP filter interceptor API to support direct propagation of call options. This is a prerequisite for implementing [gRFC A83](https://github.com/grpc/proposal/blob/master/A83-xds-gcp-authn-filter.md#a83-xds-gcp-authentication-filter), which requires filter interceptors to append new call credentials to the existing call options during stream creation. RELEASE NOTES: N/A
1 parent 9a130aa commit 5c7f936

13 files changed

Lines changed: 88 additions & 129 deletions

File tree

internal/resolver/config_selector.go

Lines changed: 1 addition & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"sync"
2525

2626
"google.golang.org/grpc/internal/serviceconfig"
27-
"google.golang.org/grpc/metadata"
2827
"google.golang.org/grpc/resolver"
2928
)
3029

@@ -52,82 +51,7 @@ type RPCConfig struct {
5251
Context context.Context
5352
MethodConfig serviceconfig.MethodConfig // configuration to use for this RPC
5453
OnCommitted func() // Called when the RPC has been committed (retries no longer possible)
55-
Interceptor ClientInterceptor
56-
}
57-
58-
// ClientStream is the same as grpc.ClientStream, but defined here for circular
59-
// dependency reasons.
60-
type ClientStream interface {
61-
// Header returns the header metadata received from the server if there
62-
// is any. It blocks if the metadata is not ready to read.
63-
Header() (metadata.MD, error)
64-
// Trailer returns the trailer metadata from the server, if there is any.
65-
// It must only be called after stream.CloseAndRecv has returned, or
66-
// stream.Recv has returned a non-nil error (including io.EOF).
67-
Trailer() metadata.MD
68-
// CloseSend closes the send direction of the stream. It closes the stream
69-
// when non-nil error is met. It is also not safe to call CloseSend
70-
// concurrently with SendMsg.
71-
CloseSend() error
72-
// Context returns the context for this stream.
73-
//
74-
// It should not be called until after Header or RecvMsg has returned. Once
75-
// called, subsequent client-side retries are disabled.
76-
Context() context.Context
77-
// SendMsg is generally called by generated code. On error, SendMsg aborts
78-
// the stream. If the error was generated by the client, the status is
79-
// returned directly; otherwise, io.EOF is returned and the status of
80-
// the stream may be discovered using RecvMsg.
81-
//
82-
// SendMsg blocks until:
83-
// - There is sufficient flow control to schedule m with the transport, or
84-
// - The stream is done, or
85-
// - The stream breaks.
86-
//
87-
// SendMsg does not wait until the message is received by the server. An
88-
// untimely stream closure may result in lost messages. To ensure delivery,
89-
// users should ensure the RPC completed successfully using RecvMsg.
90-
//
91-
// It is safe to have a goroutine calling SendMsg and another goroutine
92-
// calling RecvMsg on the same stream at the same time, but it is not safe
93-
// to call SendMsg on the same stream in different goroutines. It is also
94-
// not safe to call CloseSend concurrently with SendMsg.
95-
SendMsg(m any) error
96-
// RecvMsg blocks until it receives a message into m or the stream is
97-
// done. It returns io.EOF when the stream completes successfully. On
98-
// any other error, the stream is aborted and the error contains the RPC
99-
// status.
100-
//
101-
// It is safe to have a goroutine calling SendMsg and another goroutine
102-
// calling RecvMsg on the same stream at the same time, but it is not
103-
// safe to call RecvMsg on the same stream in different goroutines.
104-
RecvMsg(m any) error
105-
}
106-
107-
// ClientInterceptor is an interceptor for gRPC client streams.
108-
type ClientInterceptor interface {
109-
// NewStream creates a ClientStream for an RPC.
110-
//
111-
// Implementations must delegate stream creation to the provided newStream
112-
// function. To intercept or override stream behavior, implementations
113-
// may wrap the ClientStream returned by the delegate.
114-
//
115-
// Note: RPCInfo.Context is currently unused and will be nil.
116-
//
117-
// The done function is invoked when the RPC has finished using its
118-
// underlying connection or if a connection could not be assigned. Because
119-
// interceptors operate at the application layer, RPC operations may
120-
// continue on the ClientStream even after done has been called. The
121-
// caller must ensure done is non-nil.
122-
//
123-
// To ensure RPC completion notifications propagate through the entire
124-
// interceptor chain, implementations must ensure that the done function
125-
// passed to the delegate newStream invokes the done function passed to
126-
// NewStream.
127-
NewStream(ctx context.Context, ri RPCInfo, done func(), newStream func(ctx context.Context, done func()) (ClientStream, error)) (ClientStream, error)
128-
// Close closes the interceptor. Once called, no new calls to NewStream are
129-
// accepted. Ongoing calls to NewStream are allowed to complete.
130-
Close()
54+
Interceptor any
13155
}
13256

13357
// ServerInterceptor is an interceptor for incoming RPC's on gRPC server side.

internal/transport/client_stream.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,8 @@ const nonGRPCDataMaxLen = 1024
3939
type ClientStream struct {
4040
Stream // Embed for common stream functionality.
4141

42-
ct *http2Client
43-
done chan struct{} // closed at the end of stream to unblock writers.
44-
doneFunc func() // invoked at the end of stream.
42+
ct *http2Client
43+
done chan struct{} // closed at the end of stream to unblock writers.
4544

4645
headerChan chan struct{} // closed to indicate the end of header metadata.
4746
header metadata.MD // the received header metadata

internal/transport/http2_client.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,6 @@ func (t *http2Client) newStream(ctx context.Context, callHdr *CallHdr, handler s
498498
ct: t,
499499
done: make(chan struct{}),
500500
headerChan: make(chan struct{}),
501-
doneFunc: callHdr.DoneFunc,
502501
statsHandler: handler,
503502
}
504503
s.Stream.buf.init()
@@ -999,9 +998,6 @@ func (t *http2Client) closeStream(s *ClientStream, err error, rst bool, rstCode
999998
t.controlBuf.executeAndPut(addBackStreamQuota, cleanup)
1000999
// This will unblock write.
10011000
close(s.done)
1002-
if s.doneFunc != nil {
1003-
s.doneFunc()
1004-
}
10051001
}
10061002

10071003
// Close kicks off the shutdown process of the transport. This should be called

internal/transport/transport.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -594,8 +594,6 @@ type CallHdr struct {
594594

595595
PreviousAttempts int // value of grpc-previous-rpc-attempts header to set
596596

597-
DoneFunc func() // called when the stream is finished
598-
599597
// Authority is used to explicitly override the `:authority` header.
600598
//
601599
// This value comes from one of two sources:

internal/xds/httpfilter/extproc/ext_proc.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ import (
2626
"google.golang.org/grpc"
2727
"google.golang.org/grpc/internal/envconfig"
2828
"google.golang.org/grpc/internal/optional"
29-
"google.golang.org/grpc/internal/resolver"
3029
"google.golang.org/grpc/internal/xds/httpfilter"
3130
"google.golang.org/grpc/internal/xds/matcher"
3231
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
@@ -201,7 +200,7 @@ type clientFilter struct{}
201200

202201
func (clientFilter) Close() {}
203202

204-
func (clientFilter) BuildClientInterceptor(base, override httpfilter.FilterConfig) (resolver.ClientInterceptor, error) {
203+
func (clientFilter) BuildClientInterceptor(base, override httpfilter.FilterConfig) (httpfilter.ClientInterceptor, error) {
205204
b, ok := base.(baseConfig)
206205
if !ok {
207206
return nil, fmt.Errorf("extproc: incorrect config type provided (%T): %v", base, base)
@@ -230,7 +229,7 @@ func (clientFilter) BuildClientInterceptor(base, override httpfilter.FilterConfi
230229
}
231230

232231
type clientInterceptor struct {
233-
resolver.ClientInterceptor
232+
httpfilter.ClientInterceptor
234233
config baseConfig
235234
extClient v3procservicegrpc.ExternalProcessorClient
236235
cancel func() error

internal/xds/httpfilter/fault/fault.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import (
2929
"sync/atomic"
3030
"time"
3131

32+
"google.golang.org/grpc"
3233
"google.golang.org/grpc/codes"
3334
iresolver "google.golang.org/grpc/internal/resolver"
3435
"google.golang.org/grpc/internal/xds/httpfilter"
@@ -114,7 +115,7 @@ type clientFilter struct{}
114115

115116
func (clientFilter) Close() {}
116117

117-
func (clientFilter) BuildClientInterceptor(cfg, override httpfilter.FilterConfig) (iresolver.ClientInterceptor, error) {
118+
func (clientFilter) BuildClientInterceptor(cfg, override httpfilter.FilterConfig) (httpfilter.ClientInterceptor, error) {
118119
if cfg == nil {
119120
return nil, fmt.Errorf("fault: nil config provided")
120121
}
@@ -147,12 +148,12 @@ type interceptor struct {
147148

148149
var activeFaults uint32 // global active faults; accessed atomically
149150

150-
func (i *interceptor) NewStream(ctx context.Context, _ iresolver.RPCInfo, done func(), newStream func(ctx context.Context, done func()) (iresolver.ClientStream, error)) (iresolver.ClientStream, error) {
151+
func (i *interceptor) NewStream(ctx context.Context, _ iresolver.RPCInfo, newStream func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStream, error), opts ...grpc.CallOption) (grpc.ClientStream, error) {
151152
if maxAF := i.config.GetMaxActiveFaults(); maxAF != nil {
152153
defer atomic.AddUint32(&activeFaults, ^uint32(0)) // decrement counter
153154
if af := atomic.AddUint32(&activeFaults, 1); af > maxAF.GetValue() {
154155
// Would exceed maximum active fault limit.
155-
return newStream(ctx, done)
156+
return newStream(ctx, opts...)
156157
}
157158
}
158159

@@ -166,7 +167,7 @@ func (i *interceptor) NewStream(ctx context.Context, _ iresolver.RPCInfo, done f
166167
}
167168
return nil, err
168169
}
169-
return newStream(ctx, done)
170+
return newStream(ctx, opts...)
170171
}
171172

172173
func (i *interceptor) Close() {}

internal/xds/httpfilter/httpfilter.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@
2121
package httpfilter
2222

2323
import (
24+
"context"
25+
26+
"google.golang.org/grpc"
2427
iresolver "google.golang.org/grpc/internal/resolver"
2528
"google.golang.org/protobuf/proto"
2629
)
@@ -66,6 +69,24 @@ type Builder interface {
6669
IsTerminal() bool
6770
}
6871

72+
// ClientInterceptor is an interceptor for gRPC client streams.
73+
type ClientInterceptor interface {
74+
// NewStream creates a ClientStream for an RPC.
75+
//
76+
// Implementations may delegate stream creation to the provided newStream
77+
// function, passing the provided CallOption slice along with any new
78+
// CallOption instances they wish to add. To intercept or override stream
79+
// behavior, implementations may wrap the ClientStream returned by the
80+
// delegate.
81+
//
82+
// Note: RPCInfo.Context is currently unused and will be nil.
83+
NewStream(ctx context.Context, ri iresolver.RPCInfo, newStream func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStream, error), opts ...grpc.CallOption) (grpc.ClientStream, error)
84+
85+
// Close closes the interceptor. Once called, no new calls to NewStream are
86+
// accepted. Ongoing calls to NewStream are allowed to complete.
87+
Close()
88+
}
89+
6990
// ClientFilterBuilder is an optional interface that a Builder can implement to
7091
// indicate its capability to build client-side filters.
7192
type ClientFilterBuilder interface {
@@ -84,7 +105,7 @@ type ClientFilter interface {
84105
//
85106
// It is valid for this method to return a nil Interceptor and a nil error.
86107
// In this case, the RPC will not be intercepted by this filter.
87-
BuildClientInterceptor(config, override FilterConfig) (iresolver.ClientInterceptor, error)
108+
BuildClientInterceptor(config, override FilterConfig) (ClientInterceptor, error)
88109

89110
// Close is called when the filter is no longer needed.
90111
Close()

internal/xds/httpfilter/router/router.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ type filter struct{}
9191

9292
func (filter) Close() {}
9393

94-
func (filter) BuildClientInterceptor(cfg, override httpfilter.FilterConfig) (iresolver.ClientInterceptor, error) {
94+
func (filter) BuildClientInterceptor(cfg, override httpfilter.FilterConfig) (httpfilter.ClientInterceptor, error) {
9595
if _, ok := cfg.(config); !ok {
9696
return nil, fmt.Errorf("router: incorrect config type provided (%T): %v", cfg, cfg)
9797
}

internal/xds/resolver/cluster_specifier_plugin_test.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"testing"
2626

2727
"github.com/google/uuid"
28+
"google.golang.org/grpc"
2829
"google.golang.org/grpc/balancer"
2930
iresolver "google.golang.org/grpc/internal/resolver"
3031
"google.golang.org/grpc/internal/testutils"
@@ -438,13 +439,16 @@ func (s) TestResolverClusterSpecifierPlugin_WithFilters(t *testing.T) {
438439
if res.Interceptor == nil {
439440
t.Fatal("RPCInfo does not contain interceptors list")
440441
}
441-
442-
newStream := func(context.Context, func()) (iresolver.ClientStream, error) {
442+
newStream := func(context.Context, ...grpc.CallOption) (grpc.ClientStream, error) {
443443
return nil, nil
444444
}
445445

446-
if _, err = res.Interceptor.NewStream(ctx, iresolver.RPCInfo{Method: "/service/method", Context: ctx}, func() {}, newStream); err != nil {
447-
t.Fatalf("NewStream() failed with error: %v", err)
446+
if interceptor, ok := res.Interceptor.(httpfilter.ClientInterceptor); ok {
447+
if _, err = interceptor.NewStream(ctx, iresolver.RPCInfo{Method: "/service/method", Context: ctx}, newStream); err != nil {
448+
t.Fatalf("NewStream() failed with error: %v", err)
449+
}
450+
} else {
451+
t.Fatalf("res.Interceptor is type %T, want httpfilter.ClientInterceptor", res.Interceptor)
448452
}
449453

450454
// Verify that first filter receives the config.

internal/xds/resolver/serviceconfig.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"google.golang.org/grpc/internal/wrr"
3636
"google.golang.org/grpc/internal/xds/balancer/clusterimpl"
3737
"google.golang.org/grpc/internal/xds/balancer/clustermanager"
38+
"google.golang.org/grpc/internal/xds/httpfilter"
3839
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
3940
"google.golang.org/grpc/metadata"
4041
"google.golang.org/grpc/status"
@@ -104,15 +105,15 @@ type virtualHost struct {
104105

105106
// routeCluster holds information about a cluster as referenced by a route.
106107
type routeCluster struct {
107-
name string // Name of the cluster.
108-
interceptor iresolver.ClientInterceptor // HTTP filters to run for RPCs matching this route.
108+
name string // Name of the cluster.
109+
interceptor httpfilter.ClientInterceptor // HTTP filters to run for RPCs matching this route.
109110
}
110111

111112
type route struct {
112-
m *xdsresource.CompositeMatcher // converted from route matchers
113-
actionType xdsresource.RouteActionType // holds route action type
114-
clusters wrr.WRR // holds *routeCluster entries
115-
interceptors []iresolver.ClientInterceptor // Interceptors across clusters belonging to this route
113+
m *xdsresource.CompositeMatcher // converted from route matchers
114+
actionType xdsresource.RouteActionType // holds route action type
115+
clusters wrr.WRR // holds *routeCluster entries
116+
interceptors []httpfilter.ClientInterceptor // Interceptors across clusters belonging to this route
116117
maxStreamDuration time.Duration
117118
retryConfig *xdsresource.RetryConfig
118119
hashPolicies []*xdsresource.HashPolicy

0 commit comments

Comments
 (0)