Skip to content

Commit 89d4d61

Browse files
xds/resolver: ensure OnCommitted is invoked on early stream creation failure to prune unreferenced clusters. (grpc#9140)
1 parent 622a41d commit 89d4d61

4 files changed

Lines changed: 549 additions & 161 deletions

File tree

internal/xds/resolver/serviceconfig.go

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"math/bits"
2525
rand "math/rand/v2"
2626
"strings"
27+
"sync"
2728
"time"
2829

2930
xxhash "github.com/cespare/xxhash/v2"
@@ -196,14 +197,6 @@ func (cs *configSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*iresolver.RP
196197
return nil, annotateErrorWithNodeID(status.Errorf(codes.Internal, "error retrieving cluster for match: %v (%T)", cluster, cluster), cs.xdsNodeID)
197198
}
198199

199-
// Add a ref to the selected cluster/plugin, as this RPC needs this
200-
// cluster/plugin until it is committed.
201-
if info, ok := cs.clusters[cluster.name]; ok {
202-
info.refCount.Add(1)
203-
} else if info, ok := cs.plugins[cluster.name]; ok {
204-
info.refCount.Add(1)
205-
}
206-
207200
lbCtx := clustermanager.SetPickedCluster(rpcInfo.Context, cluster.name)
208201
lbCtx = xdsresource.NewContextWithXDSConfig(lbCtx, cs.xdsConfig)
209202
lbCtx = iringhash.SetXDSRequestHash(lbCtx, cs.generateHash(rpcInfo, rt.hashPolicies))
@@ -212,31 +205,36 @@ func (cs *configSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*iresolver.RP
212205
}
213206

214207
config := &iresolver.RPCConfig{
215-
// Communicate to the LB policy the chosen cluster and request hash, if Ring Hash LB policy.
216-
Context: lbCtx,
217-
OnCommitted: func() {
218-
// When the RPC is committed, the cluster is no longer required.
219-
// Decrease its ref.
220-
if info, ok := cs.clusters[cluster.name]; ok {
221-
if v := info.refCount.Add(-1); v == 0 {
222-
// We call unsubscribe rather than sendNewServiceConfig to
223-
// prevent redundant updates. If the reference count in the
224-
// dependency manager drops to zero, it will automatically
225-
// trigger a service config update with this cluster
226-
// removed. Calling unsubscribe allows the dependency
227-
// manager to handle the update flow once and for all.
228-
info.unsubscribe()
229-
}
208+
Context: lbCtx,
209+
Interceptor: cluster.interceptor,
210+
}
211+
212+
if info, ok := cs.clusters[cluster.name]; ok {
213+
// Add a ref to the selected cluster, as this RPC needs this
214+
// cluster until it is committed.
215+
info.refCount.Add(1)
216+
config.OnCommitted = sync.OnceFunc(func() {
217+
if v := info.refCount.Add(-1); v == 0 {
218+
// We call unsubscribe rather than sendNewServiceConfig to
219+
// prevent redundant updates. If the reference count in the
220+
// dependency manager drops to zero, it will automatically
221+
// trigger a service config update with this cluster
222+
// removed. Calling unsubscribe allows the dependency
223+
// manager to handle the update flow once and for all.
224+
info.unsubscribe()
230225
}
231-
if info, ok := cs.plugins[cluster.name]; ok {
232-
if v := info.refCount.Add(-1); v == 0 {
233-
// This entry will be removed from activePlugins when
234-
// producing a new service config update.
235-
cs.sendNewServiceConfig()
236-
}
226+
})
227+
} else if info, ok := cs.plugins[cluster.name]; ok {
228+
// Add a ref to the selected plugin, as this RPC needs this
229+
// plugin until it is committed.
230+
info.refCount.Add(1)
231+
config.OnCommitted = sync.OnceFunc(func() {
232+
if v := info.refCount.Add(-1); v == 0 {
233+
// This entry will be removed from activePlugins when
234+
// producing a new service config update.
235+
cs.sendNewServiceConfig()
237236
}
238-
},
239-
Interceptor: cluster.interceptor,
237+
})
240238
}
241239

242240
if rt.maxStreamDuration != 0 {

internal/xds/resolver/xds_resolver_test.go

Lines changed: 0 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -935,128 +935,6 @@ func (s) TestResolverMaxStreamDuration(t *testing.T) {
935935
}
936936
}
937937

938-
// Tests that clusters remain in service config if RPCs are in flight.
939-
func (s) TestResolverDelayedOnCommitted(t *testing.T) {
940-
// Spin up an xDS management server for the test.
941-
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
942-
defer cancel()
943-
nodeID := uuid.New().String()
944-
mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID)
945-
946-
// Configure resources on the management server.
947-
resources := e2e.DefaultClientResources(e2e.ResourceParams{
948-
DialTarget: defaultTestServiceName,
949-
NodeID: nodeID,
950-
Host: defaultTestHostname,
951-
Port: defaultTestPort[0],
952-
SecLevel: e2e.SecurityLevelNone,
953-
})
954-
if err := mgmtServer.Update(ctx, resources); err != nil {
955-
t.Fatal(err)
956-
}
957-
958-
stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)
959-
960-
// Read the update pushed by the resolver to the ClientConn.
961-
cs := verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
962-
963-
// Make an RPC, but do not commit it yet.
964-
resOld, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"})
965-
if err != nil {
966-
t.Fatalf("cs.SelectConfig(): %v", err)
967-
}
968-
wantClusterName := fmt.Sprintf("cluster:%s", resources.Clusters[0].Name)
969-
if cluster := clustermanager.PickedCluster(resOld.Context); cluster != wantClusterName {
970-
t.Fatalf("Picked cluster is %q, want %q", cluster, wantClusterName)
971-
}
972-
973-
// Delay resOld.OnCommitted(). As long as there are pending RPCs to removed
974-
// clusters, they still appear in the service config.
975-
oldClusterName := resources.Clusters[0].Name
976-
// Update the route configuration resource on the management server to
977-
// return a new cluster.
978-
newClusterName := "new-" + defaultTestClusterName
979-
newEndpointName := "new-" + defaultTestEndpointName
980-
resources.Routes = []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(resources.Routes[0].Name, defaultTestServiceName, newClusterName)}
981-
// Appending the new cluster and endpoint resources to avoid getting
982-
// resource removed errors.
983-
resources.Clusters = append(resources.Clusters, e2e.DefaultCluster(newClusterName, newEndpointName, e2e.SecurityLevelNone))
984-
resources.Endpoints = append(resources.Endpoints, e2e.DefaultEndpoint(newEndpointName, defaultTestHostname, defaultTestPort))
985-
if err := mgmtServer.Update(ctx, resources); err != nil {
986-
t.Fatal(err)
987-
}
988-
989-
// Read the update pushed by the resolver to the ClientConn and ensure the
990-
// old cluster is present in the service config. Also ensure that the newly
991-
// returned config selector does not hold a reference to the old cluster.
992-
wantSC := fmt.Sprintf(`
993-
{
994-
"loadBalancingConfig": [
995-
{
996-
"xds_cluster_manager_experimental": {
997-
"children": {
998-
"cluster:%s": {
999-
"childPolicy": [
1000-
{
1001-
"cds_experimental": {
1002-
"cluster": "%s"
1003-
}
1004-
}
1005-
]
1006-
},
1007-
"cluster:%s": {
1008-
"childPolicy": [
1009-
{
1010-
"cds_experimental": {
1011-
"cluster": "%s"
1012-
}
1013-
}
1014-
]
1015-
}
1016-
}
1017-
}
1018-
}
1019-
]
1020-
}`, oldClusterName, oldClusterName, newClusterName, newClusterName)
1021-
cs = verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
1022-
1023-
resNew, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"})
1024-
if err != nil {
1025-
t.Fatalf("cs.SelectConfig(): %v", err)
1026-
}
1027-
wantClusterName = fmt.Sprintf("cluster:%s", newClusterName)
1028-
if cluster := clustermanager.PickedCluster(resNew.Context); cluster != wantClusterName {
1029-
t.Fatalf("Picked cluster is %q, want %q", cluster, wantClusterName)
1030-
}
1031-
1032-
// Invoke OnCommitted on the old RPC; should lead to a service config update
1033-
// that deletes the old cluster, as the old cluster no longer has any
1034-
// pending RPCs.
1035-
resOld.OnCommitted()
1036-
1037-
wantSC = fmt.Sprintf(`
1038-
{
1039-
"loadBalancingConfig": [
1040-
{
1041-
"xds_cluster_manager_experimental": {
1042-
"children": {
1043-
"cluster:%s": {
1044-
"childPolicy": [
1045-
{
1046-
"cds_experimental": {
1047-
"cluster": "%s"
1048-
}
1049-
}
1050-
]
1051-
}
1052-
}
1053-
}
1054-
}
1055-
]
1056-
}`, newClusterName, newClusterName)
1057-
verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
1058-
}
1059-
1060938
// Tests the case where two LDS updates with the same RDS name to watch are
1061939
// received without an RDS in between. Those LDS updates shouldn't trigger a
1062940
// service config update.

stream.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -328,10 +328,7 @@ func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth
328328

329329
mc := &emptyMethodConfig
330330
var onCommit func()
331-
newStream := func(ctx context.Context, filterOpts ...CallOption) (ClientStream, error) {
332-
if filterOpts != nil {
333-
opts = combine(opts, filterOpts)
334-
}
331+
newStream := func(ctx context.Context, opts ...CallOption) (ClientStream, error) {
335332
return newClientStreamWithParams(ctx, desc, cc, method, mc, onCommit, nameResolutionDelayed, opts...)
336333
}
337334

@@ -353,13 +350,23 @@ func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth
353350
ctx = rpcConfig.Context
354351
}
355352
mc = &rpcConfig.MethodConfig
356-
onCommit = rpcConfig.OnCommitted
353+
354+
if rpcConfig.OnCommitted != nil {
355+
onCommit = rpcConfig.OnCommitted
356+
// Register an OnFinish CallOption with the OnCommitted callback to
357+
// ensure it is invoked on stream termination, even if the stream
358+
// fails early before committing. Implementations of OnCommitted are
359+
// expected to be idempotent (e.g., guarded by sync.Once), since both
360+
// onCommit and OnFinish may run for a single RPC.
361+
opts = append(opts, OnFinish(func(error) { rpcConfig.OnCommitted() }))
362+
}
363+
357364
if rpcConfig.Interceptor != nil {
358365
rpcInfo.Context = nil
359366
ns := newStream
360367
if interceptor, ok := rpcConfig.Interceptor.(clientInterceptor); ok {
361-
newStream = func(ctx context.Context, filterOpts ...CallOption) (ClientStream, error) {
362-
cs, err := interceptor.NewStream(ctx, rpcInfo, ns, filterOpts...)
368+
newStream = func(ctx context.Context, opts ...CallOption) (ClientStream, error) {
369+
cs, err := interceptor.NewStream(ctx, rpcInfo, ns, opts...)
363370
if err != nil {
364371
return nil, toRPCErr(err)
365372
}
@@ -371,7 +378,7 @@ func newClientStream(ctx context.Context, desc *StreamDesc, cc *ClientConn, meth
371378
}
372379
}
373380

374-
return newStream(ctx)
381+
return newStream(ctx, opts...)
375382
}
376383

377384
func newClientStreamWithParams(ctx context.Context, desc *StreamDesc, cc *ClientConn, method string, mc *serviceconfig.MethodConfig, onCommit func(), nameResolutionDelayed bool, opts ...CallOption) (_ ClientStream, err error) {

0 commit comments

Comments
 (0)