snapshot of https://github.com/grpc/grpc-go.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (push) Failing after 1s
Testing / vet-proto (push) Failing after 1s
Testing / tests (arm, latest) (push) Waiting to run
Testing / extras (latest) (push) Failing after 1s
Testing / static checks (latest-1) (push) Failing after 1s
Testing / tests (-race, latest) (push) Failing after 1s
Testing / tests (i386, latest) (push) Failing after 1s
Testing / tests (latest) (push) Failing after 1s
Testing / tests (latest-1) (push) Failing after 1s
Lock Threads / lock (push) Failing after 1s
govulncheck / govulncheck (.) (push) Failing after 1s
govulncheck / govulncheck (./cmd/protoc-gen-go-grpc) (push) Failing after 1s
govulncheck / govulncheck (./gcp/observability) (push) Failing after 1s
govulncheck / govulncheck (./security/advancedtls) (push) Failing after 1s
govulncheck / govulncheck (./stats/opencensus) (push) Failing after 1s
Stale bot / stale (push) Failing after 1s

This commit is contained in:
upstream-mirror
2026-09-15 20:40:15 +05:00
commit e241b33f26
1365 changed files with 339570 additions and 0 deletions

View File

@@ -0,0 +1,479 @@
/*
*
* Copyright 2021 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package resolver_test
import (
"context"
"encoding/json"
"fmt"
"testing"
"github.com/google/uuid"
"google.golang.org/grpc"
"google.golang.org/grpc/balancer"
iresolver "google.golang.org/grpc/internal/resolver"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/testutils/xds/e2e"
"google.golang.org/grpc/internal/xds/balancer/clustermanager"
"google.golang.org/grpc/internal/xds/clusterspecifier"
"google.golang.org/grpc/internal/xds/httpfilter"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/anypb"
"google.golang.org/protobuf/types/known/wrapperspb"
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
)
func init() {
balancer.Register(cspBalancerBuilder{})
clusterspecifier.Register(testClusterSpecifierPlugin{})
}
// cspBalancerBuilder is a no-op LB policy which is referenced by the
// testClusterSpecifierPlugin.
type cspBalancerBuilder struct{}
func (cspBalancerBuilder) Build(balancer.ClientConn, balancer.BuildOptions) balancer.Balancer {
return nil
}
func (cspBalancerBuilder) Name() string {
return "csp_experimental"
}
type cspBalancerConfig struct {
serviceconfig.LoadBalancingConfig
ArbitraryField string `json:"arbitrary_field"`
}
func (cspBalancerBuilder) ParseConfig(lbCfg json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
cfg := &cspBalancerConfig{}
if err := json.Unmarshal(lbCfg, cfg); err != nil {
return nil, err
}
return cfg, nil
}
// testClusterSpecifierPlugin is a test cluster specifier plugin which returns
// an LB policy configuration specifying the cspBalancer.
type testClusterSpecifierPlugin struct {
}
func (testClusterSpecifierPlugin) TypeURLs() []string {
// The config for this plugin contains a wrapperspb.StringValue, and since
// we marshal that proto as an Any proto, the type URL on the latter gets
// set to "type.googleapis.com/google.protobuf.StringValue". If we wanted a
// more descriptive type URL for this test plugin, we would have to define a
// proto package with a message for the configuration. That would be
// overkill for a test. Therefore, this seems to be an acceptable tradeoff.
return []string{"type.googleapis.com/google.protobuf.StringValue"}
}
func (testClusterSpecifierPlugin) ParseClusterSpecifierConfig(cfg proto.Message) (clusterspecifier.BalancerConfig, error) {
if cfg == nil {
return nil, fmt.Errorf("testClusterSpecifierPlugin: nil configuration message provided")
}
anyp, ok := cfg.(*anypb.Any)
if !ok {
return nil, fmt.Errorf("testClusterSpecifierPlugin: error parsing config %v: got type %T, want *anypb.Any", cfg, cfg)
}
lbCfg := new(wrapperspb.StringValue)
if err := anypb.UnmarshalTo(anyp, lbCfg, proto.UnmarshalOptions{}); err != nil {
return nil, fmt.Errorf("testClusterSpecifierPlugin: error parsing config %v: %v", cfg, err)
}
return []map[string]any{{"csp_experimental": cspBalancerConfig{ArbitraryField: lbCfg.GetValue()}}}, nil
}
// TestResolverClusterSpecifierPlugin tests the case where a route configuration
// containing cluster specifier plugins is sent by the management server. The
// test verifies that the service config output by the resolver contains the LB
// policy specified by the cluster specifier plugin, and the config selector
// returns the cluster associated with the cluster specifier plugin.
//
// The test also verifies that a change in the cluster specifier plugin config
// result in appropriate change in the service config pushed by the resolver.
func (s) TestResolverClusterSpecifierPlugin(t *testing.T) {
// Spin up an xDS management server for the test.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
nodeID := uuid.New().String()
mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID)
// Configure resources on the management server.
listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)}
routes := []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{
RouteConfigName: defaultTestRouteConfigName,
ListenerName: defaultTestServiceName,
ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeClusterSpecifierPlugin,
ClusterSpecifierPluginName: "cspA",
ClusterSpecifierPluginConfig: testutils.MarshalAny(t, &wrapperspb.StringValue{Value: "anything"}),
})}
configureResources(ctx, t, mgmtServer, nodeID, listeners, routes, nil, nil)
stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)
// Wait for an update from the resolver, and verify the service config.
wantSC := `
{
"loadBalancingConfig": [
{
"xds_cluster_manager_experimental": {
"children": {
"cluster_specifier_plugin:cspA": {
"childPolicy": [
{
"csp_experimental": {
"arbitrary_field": "anything"
}
}
]
}
}
}
}
]
}`
cs := verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"})
if err != nil {
t.Fatalf("cs.SelectConfig(): %v", err)
}
gotCluster := clustermanager.PickedCluster(res.Context)
wantCluster := "cluster_specifier_plugin:cspA"
if gotCluster != wantCluster {
t.Fatalf("config selector returned cluster: %v, want: %v", gotCluster, wantCluster)
}
// Change the cluster specifier plugin configuration.
routes = []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{
RouteConfigName: defaultTestRouteConfigName,
ListenerName: defaultTestServiceName,
ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeClusterSpecifierPlugin,
ClusterSpecifierPluginName: "cspA",
ClusterSpecifierPluginConfig: testutils.MarshalAny(t, &wrapperspb.StringValue{Value: "changed"}),
})}
configureResources(ctx, t, mgmtServer, nodeID, listeners, routes, nil, nil)
// Wait for an update from the resolver, and verify the service config.
wantSC = `
{
"loadBalancingConfig": [
{
"xds_cluster_manager_experimental": {
"children": {
"cluster_specifier_plugin:cspA": {
"childPolicy": [
{
"csp_experimental": {
"arbitrary_field": "changed"
}
}
]
}
}
}
}
]
}`
verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
}
// TestXDSResolverDelayedOnCommittedCSP tests that cluster specifier plugins and
// their corresponding configurations remain in service config if RPCs are in
// flight.
func (s) TestXDSResolverDelayedOnCommittedCSP(t *testing.T) {
// Spin up an xDS management server for the test.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
nodeID := uuid.New().String()
mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID)
// Configure resources on the management server.
listeners := []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, defaultTestRouteConfigName)}
routes := []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{
RouteConfigName: defaultTestRouteConfigName,
ListenerName: defaultTestServiceName,
ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeClusterSpecifierPlugin,
ClusterSpecifierPluginName: "cspA",
ClusterSpecifierPluginConfig: testutils.MarshalAny(t, &wrapperspb.StringValue{Value: "anythingA"}),
})}
configureResources(ctx, t, mgmtServer, nodeID, listeners, routes, nil, nil)
stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)
// Wait for an update from the resolver, and verify the service config.
wantSC := `
{
"loadBalancingConfig": [
{
"xds_cluster_manager_experimental": {
"children": {
"cluster_specifier_plugin:cspA": {
"childPolicy": [
{
"csp_experimental": {
"arbitrary_field": "anythingA"
}
}
]
}
}
}
}
]
}`
cs := verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
resOld, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"})
if err != nil {
t.Fatalf("cs.SelectConfig(): %v", err)
}
gotCluster := clustermanager.PickedCluster(resOld.Context)
wantCluster := "cluster_specifier_plugin:cspA"
if gotCluster != wantCluster {
t.Fatalf("config selector returned cluster: %v, want: %v", gotCluster, wantCluster)
}
// Delay resOld.OnCommitted(). As long as there are pending RPCs to removed
// clusters, they still appear in the service config.
// Change the cluster specifier plugin configuration.
routes = []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{
RouteConfigName: defaultTestRouteConfigName,
ListenerName: defaultTestServiceName,
ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeClusterSpecifierPlugin,
ClusterSpecifierPluginName: "cspB",
ClusterSpecifierPluginConfig: testutils.MarshalAny(t, &wrapperspb.StringValue{Value: "anythingB"}),
})}
configureResources(ctx, t, mgmtServer, nodeID, listeners, routes, nil, nil)
// Wait for an update from the resolver, and verify the service config.
wantSC = `
{
"loadBalancingConfig": [
{
"xds_cluster_manager_experimental": {
"children": {
"cluster_specifier_plugin:cspA": {
"childPolicy": [
{
"csp_experimental": {
"arbitrary_field": "anythingA"
}
}
]
},
"cluster_specifier_plugin:cspB": {
"childPolicy": [
{
"csp_experimental": {
"arbitrary_field": "anythingB"
}
}
]
}
}
}
}
]
}`
cs = verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
// Perform an RPC and ensure that it is routed to the new cluster.
resNew, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"})
if err != nil {
t.Fatalf("cs.SelectConfig(): %v", err)
}
gotCluster = clustermanager.PickedCluster(resNew.Context)
wantCluster = "cluster_specifier_plugin:cspB"
if gotCluster != wantCluster {
t.Fatalf("config selector returned cluster: %v, want: %v", gotCluster, wantCluster)
}
// Invoke resOld.OnCommitted; should lead to a service config update that deletes
// cspA.
resOld.OnCommitted()
wantSC = `
{
"loadBalancingConfig": [
{
"xds_cluster_manager_experimental": {
"children": {
"cluster_specifier_plugin:cspB": {
"childPolicy": [
{
"csp_experimental": {
"arbitrary_field": "anythingB"
}
}
]
}
}
}
}
]
}`
verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
}
// TestResolverClusterSpecifierPlugin_WithFilters tests the case where a route
// configuration containing cluster specifier plugins is sent by the management
// server, and HTTP filters are configured. The test verifies that the
// interceptor chain is built for routes matching cluster specifier plugins.
func (s) TestResolverClusterSpecifierPlugin_WithFilters(t *testing.T) {
// Register custom httpFilter builders for the test.
testFilterTypeURL1 := "test-filter-type-url-1" + uuid.New().String()
testFilterTypeURL2 := "test-filter-type-url-2" + uuid.New().String()
newStreamChan := testutils.NewChannelWithSize(2)
fb1 := &testHTTPFilterWithRPCMetadata{
logger: t,
typeURL: testFilterTypeURL1,
newStreamChan: newStreamChan,
}
fb2 := &testHTTPFilterWithRPCMetadata{
logger: t,
typeURL: testFilterTypeURL2,
newStreamChan: newStreamChan,
}
httpfilter.Register(fb1)
httpfilter.Register(fb2)
defer httpfilter.UnregisterForTesting(fb1.typeURL)
defer httpfilter.UnregisterForTesting(fb2.typeURL)
// Spin up an xDS management server.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
nodeID := uuid.New().String()
mgmtServer, _, _, bc := setupManagementServerForTest(t, nodeID)
// Configure resources on the management server.
// We need a listener with the filter, and a route with ClusterSpecifierPlugin.
listeners := []*v3listenerpb.Listener{{
Name: defaultTestServiceName,
ApiListener: &v3listenerpb.ApiListener{
ApiListener: testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
RouteSpecifier: &v3httppb.HttpConnectionManager_Rds{Rds: &v3httppb.Rds{
ConfigSource: &v3corepb.ConfigSource{
ConfigSourceSpecifier: &v3corepb.ConfigSource_Ads{Ads: &v3corepb.AggregatedConfigSource{}},
},
RouteConfigName: defaultTestRouteConfigName,
}},
HttpFilters: []*v3httppb.HttpFilter{
newHTTPFilter(t, "test-filter-1", testFilterTypeURL1, "filter-path-1", ""),
newHTTPFilter(t, "test-filter-2", testFilterTypeURL2, "filter-path-2", ""),
e2e.RouterHTTPFilter,
},
}),
},
}}
routes := []*v3routepb.RouteConfiguration{e2e.RouteConfigResourceWithOptions(e2e.RouteConfigOptions{
RouteConfigName: defaultTestRouteConfigName,
ListenerName: defaultTestServiceName,
ClusterSpecifierType: e2e.RouteConfigClusterSpecifierTypeClusterSpecifierPlugin,
ClusterSpecifierPluginName: "cspA",
ClusterSpecifierPluginConfig: testutils.MarshalAny(t, &wrapperspb.StringValue{Value: "anything"}),
})}
// Override the configuration for "test-filter-1" in the route.
routes[0].VirtualHosts[0].Routes[0].TypedPerFilterConfig = map[string]*anypb.Any{
"test-filter-1": newHTTPFilter(t, "test-filter-1", testFilterTypeURL1, "override-path-1", "").GetTypedConfig(),
}
configureResources(ctx, t, mgmtServer, nodeID, listeners, routes, nil, nil)
stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)
// Wait for an update from the resolver, and verify the service config.
wantSC := `
{
"loadBalancingConfig": [
{
"xds_cluster_manager_experimental": {
"children": {
"cluster_specifier_plugin:cspA": {
"childPolicy": [
{
"csp_experimental": {
"arbitrary_field": "anything"
}
}
]
}
}
}
}
]
}`
cs := verifyUpdateFromResolver(ctx, t, stateCh, wantSC)
res, err := cs.SelectConfig(iresolver.RPCInfo{Context: ctx, Method: "/service/method"})
if err != nil {
t.Fatalf("cs.SelectConfig(): %v", err)
}
// Verify that the interceptor is not nil.
if res.Interceptor == nil {
t.Fatal("RPCInfo does not contain interceptors list")
}
newStream := func(context.Context, ...grpc.CallOption) (grpc.ClientStream, error) {
return nil, nil
}
if interceptor, ok := res.Interceptor.(httpfilter.ClientInterceptor); ok {
if _, err = interceptor.NewStream(ctx, iresolver.RPCInfo{Method: "/service/method", Context: ctx}, newStream); err != nil {
t.Fatalf("NewStream() failed with error: %v", err)
}
} else {
t.Fatalf("res.Interceptor is type %T, want httpfilter.ClientInterceptor", res.Interceptor)
}
// Verify that first filter receives the config.
cfg, err := newStreamChan.Receive(ctx)
if err != nil {
t.Fatalf("Timeout waiting for first filter to receive config: %v", err)
}
ofc := cfg.(overallFilterConfig)
if ofc.BasePath != "filter-path-1" {
t.Fatalf("Unexpected base path for first filter, got: %q, want: %q", ofc.BasePath, "filter-path-1")
}
if ofc.OverridePath != "override-path-1" {
t.Fatalf("Unexpected override path for first filter, got: %q, want: %q", ofc.OverridePath, "override-path-1")
}
// Verify that second filter receives the base path.
cfg, err = newStreamChan.Receive(ctx)
if err != nil {
t.Fatalf("Timeout waiting for second filter to receive config: %v", err)
}
ofc = cfg.(overallFilterConfig)
if ofc.BasePath != "filter-path-2" {
t.Fatalf("Unexpected base path for second filter, got: %q, want: %q", ofc.BasePath, "filter-path-2")
}
if ofc.OverridePath != "" {
t.Fatalf("Unexpected override path for second filter, got: %q, want: %q", ofc.OverridePath, "")
}
}

View File

@@ -0,0 +1,288 @@
/*
*
* Copyright 2023 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package resolver_test
import (
"context"
"fmt"
"net/url"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/grpctest"
iresolver "google.golang.org/grpc/internal/resolver"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/testutils/xds/e2e"
xdsresolver "google.golang.org/grpc/internal/xds/resolver"
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
"google.golang.org/grpc/resolver"
"google.golang.org/grpc/serviceconfig"
"google.golang.org/grpc/status"
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
v3endpointpb "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
v3discoverypb "github.com/envoyproxy/go-control-plane/envoy/service/discovery/v3"
)
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
const (
defaultTestTimeout = 10 * time.Second
defaultTestShortTimeout = 100 * time.Microsecond
defaultTestServiceName = "service-name"
defaultTestRouteConfigName = "route-config-name"
defaultTestClusterName = "cluster-name"
defaultTestEndpointName = "endpoint-name"
defaultTestHostname = "test-host"
)
var defaultTestPort = []uint32{8080}
// wantServiceConfig returns a JSON representation of a service config with
// xds_cluster_manager_experimental LB policy with a child policy of
// cds_experimental for the provided cluster name.
func wantServiceConfig(clusterName string) string {
return fmt.Sprintf(`{
"loadBalancingConfig": [{
"xds_cluster_manager_experimental": {
"children": {
"cluster:%s": {
"childPolicy": [{
"cds_experimental": {
"cluster": "%s"
}
}]
}
}
}
}]
}`, clusterName, clusterName)
}
// buildResolverForTarget builds an xDS resolver for the given target. If
// the bootstrap contents are provided, it build the xDS resolver using them
// otherwise, it uses the default xDS resolver.
//
// It returns the following:
// - a channel to read updates from the resolver
// - a channel to read errors from the resolver
// - the newly created xDS resolver
func buildResolverForTarget(t *testing.T, target resolver.Target, bootstrapContents []byte) (chan resolver.State, chan error, resolver.Resolver) {
t.Helper()
var builder resolver.Builder
if bootstrapContents != nil {
// Create an xDS resolver with the provided bootstrap configuration.
var err error
builder, err = internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents)
if err != nil {
t.Fatalf("Failed to create xDS resolver for testing: %v", err)
}
} else {
builder = resolver.Get(xdsresolver.Scheme)
if builder == nil {
t.Fatalf("Scheme %q is not registered", xdsresolver.Scheme)
}
}
stateCh := make(chan resolver.State, 1)
updateStateF := func(s resolver.State) error {
stateCh <- s
return nil
}
errCh := make(chan error, 1)
reportErrorF := func(err error) {
select {
case errCh <- err:
default:
}
}
tcc := &testutils.ResolverClientConn{Logger: t, UpdateStateF: updateStateF, ReportErrorF: reportErrorF}
r, err := builder.Build(target, tcc, resolver.BuildOptions{
Authority: url.PathEscape(target.Endpoint()),
})
if err != nil {
t.Fatalf("Failed to build xDS resolver for target %q: %v", target, err)
}
t.Cleanup(r.Close)
return stateCh, errCh, r
}
// verifyUpdateFromResolver waits for the resolver to push an update to the fake
// resolver.ClientConn and verifies that update matches the provided service
// config.
//
// Tests that want to skip verifying the contents of the service config can pass
// an empty string.
//
// Returns the config selector from the state update pushed by the resolver.
// Tests that don't need the config selector can ignore the return value.
func verifyUpdateFromResolver(ctx context.Context, t *testing.T, stateCh chan resolver.State, wantSC string) iresolver.ConfigSelector {
t.Helper()
var state resolver.State
select {
case <-ctx.Done():
t.Fatalf("Timeout waiting for an update from the resolver: %v", ctx.Err())
case state = <-stateCh:
if err := state.ServiceConfig.Err; err != nil {
t.Fatalf("Received error in service config: %v", state.ServiceConfig.Err)
}
if wantSC == "" {
break
}
wantSCParsed := internal.ParseServiceConfig.(func(string) *serviceconfig.ParseResult)(wantSC)
if !internal.EqualServiceConfigForTesting(state.ServiceConfig.Config, wantSCParsed.Config) {
t.Fatalf("Got service config:\n%s \nWant service config:\n%s", cmp.Diff(nil, state.ServiceConfig.Config), cmp.Diff(nil, wantSCParsed.Config))
}
}
cs := iresolver.GetConfigSelector(state)
if cs == nil {
t.Fatal("Received nil config selector in update from resolver")
}
return cs
}
// verifyNoUpdateFromResolver verifies that no update is pushed on stateCh.
// Calls t.Fatal() if an update is received before defaultTestShortTimeout
// expires.
func verifyNoUpdateFromResolver(ctx context.Context, t *testing.T, stateCh chan resolver.State) {
t.Helper()
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
defer sCancel()
select {
case <-sCtx.Done():
case u := <-stateCh:
t.Fatalf("Received update from resolver %v when none expected", u)
}
}
func verifyResolverError(gotErr error, wantCode codes.Code, wantErr, wantNodeID string) error {
if gotErr == nil {
return fmt.Errorf("got nil error from resolver, want error with code %v", wantCode)
}
if !strings.Contains(gotErr.Error(), wantErr) {
return fmt.Errorf("got error from resolver %q, want %q", gotErr, wantErr)
}
if gotCode := status.Code(gotErr); gotCode != wantCode {
return fmt.Errorf("got error from resolver with code %v, want %v", gotCode, wantCode)
}
if !strings.Contains(gotErr.Error(), wantNodeID) {
return fmt.Errorf("got error from resolver %q, want nodeID %q", gotErr, wantNodeID)
}
return nil
}
// Spins up an xDS management server and sets up an xDS bootstrap configuration
// file that points to it.
//
// Returns the following:
// - A reference to the xDS management server
// - A channel to read requested Listener resource names
// - A channel to read requested RouteConfiguration resource names
// - Contents of the bootstrap configuration pointing to xDS management
// server
func setupManagementServerForTest(t *testing.T, nodeID string) (*e2e.ManagementServer, chan []string, chan []string, []byte) {
t.Helper()
listenerResourceNamesCh := make(chan []string, 1)
routeConfigResourceNamesCh := make(chan []string, 1)
// Setup the management server to push the requested listener and route
// configuration resource names on to separate channels for the test to
// inspect.
mgmtServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
switch req.GetTypeUrl() {
case version.V3ListenerURL:
select {
case <-listenerResourceNamesCh:
default:
}
select {
case listenerResourceNamesCh <- req.GetResourceNames():
default:
}
case version.V3RouteConfigURL:
select {
case <-routeConfigResourceNamesCh:
default:
}
select {
case routeConfigResourceNamesCh <- req.GetResourceNames():
default:
}
}
return nil
},
AllowResourceSubset: true,
})
// Create a bootstrap configuration specifying the above management server.
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, mgmtServer.Address)
return mgmtServer, listenerResourceNamesCh, routeConfigResourceNamesCh, bootstrapContents
}
// Updates all resources on the given management server.
func configureResources(ctx context.Context, t *testing.T, mgmtServer *e2e.ManagementServer, nodeID string, listeners []*v3listenerpb.Listener, routes []*v3routepb.RouteConfiguration, clusters []*v3clusterpb.Cluster, endpoints []*v3endpointpb.ClusterLoadAssignment) {
resources := e2e.UpdateOptions{
NodeID: nodeID,
Listeners: listeners,
Routes: routes,
Clusters: clusters,
Endpoints: endpoints,
SkipValidation: true,
}
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
}
// waitForResourceNames waits for the wantNames to be pushed on to namesCh.
// Fails the test by calling t.Fatal if the context expires before that.
func waitForResourceNames(ctx context.Context, t *testing.T, namesCh chan []string, wantNames []string) {
t.Helper()
for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) {
select {
case <-ctx.Done():
case gotNames := <-namesCh:
if cmp.Equal(gotNames, wantNames, cmpopts.EquateEmpty(), cmpopts.SortSlices(func(a, b string) bool { return a < b })) {
return
}
t.Logf("Received resource names %v, want %v", gotNames, wantNames)
}
}
t.Fatalf("Timeout waiting for resource to be requested from the management server")
}

View File

@@ -0,0 +1,30 @@
/*
*
* Copyright 2023 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package internal contains functionality internal to the xDS resolver.
package internal
// The following variables are overridden in tests.
var (
// NewWRR is the function used to create a new weighted round robin
// implementation.
NewWRR any // func() wrr.WRR
// NewXDSClient is the function used to create a new xDS client.
NewXDSClient any // func(string, estats.MetricsRecorder) (xdsclient.XDSClient, func(), error)
)

View File

@@ -0,0 +1,34 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package resolver
import (
"fmt"
"google.golang.org/grpc/grpclog"
internalgrpclog "google.golang.org/grpc/internal/grpclog"
)
const prefix = "[xds-resolver %p] "
var logger = grpclog.Component("xds")
func prefixLogger(p *xdsResolver) *internalgrpclog.PrefixLogger {
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf(prefix, p))
}

View File

@@ -0,0 +1,381 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package resolver
import (
"encoding/json"
"fmt"
"math/bits"
rand "math/rand/v2"
"strings"
"sync"
"time"
xxhash "github.com/cespare/xxhash/v2"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/internal/grpcsync"
"google.golang.org/grpc/internal/grpcutil"
iresolver "google.golang.org/grpc/internal/resolver"
iringhash "google.golang.org/grpc/internal/ringhash"
"google.golang.org/grpc/internal/serviceconfig"
"google.golang.org/grpc/internal/wrr"
"google.golang.org/grpc/internal/xds/balancer/clusterimpl"
"google.golang.org/grpc/internal/xds/balancer/clustermanager"
"google.golang.org/grpc/internal/xds/httpfilter"
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
const (
cdsName = "cds_experimental"
xdsClusterManagerName = "xds_cluster_manager_experimental"
clusterPrefix = "cluster:"
clusterSpecifierPluginPrefix = "cluster_specifier_plugin:"
)
type serviceConfig struct {
LoadBalancingConfig balancerConfig `json:"loadBalancingConfig"`
}
type balancerConfig []map[string]any
func newBalancerConfig(name string, config any) balancerConfig {
return []map[string]any{{name: config}}
}
type cdsBalancerConfig struct {
Cluster string `json:"cluster"`
}
type xdsChildConfig struct {
ChildPolicy balancerConfig `json:"childPolicy"`
}
type xdsClusterManagerConfig struct {
Children map[string]xdsChildConfig `json:"children"`
}
// serviceConfigJSON produces a service config in JSON format that contains LB
// policy config for the "xds_cluster_manager" LB policy, with entries in the
// children map for all active clusters.
func serviceConfigJSON(activeClusters map[string]*clusterInfo, activePlugins map[string]*clusterInfo) []byte {
// Generate children (all entries in activeClusters).
children := make(map[string]xdsChildConfig)
for cluster, ci := range activeClusters {
children[cluster] = ci.cfg
}
for plugin, ci := range activePlugins {
children[plugin] = ci.cfg
}
sc := serviceConfig{
LoadBalancingConfig: newBalancerConfig(
xdsClusterManagerName, xdsClusterManagerConfig{Children: children},
),
}
// This is not expected to fail as we have constructed the service config by
// hand right above, and therefore ok to panic.
bs, err := json.Marshal(sc)
if err != nil {
panic(fmt.Sprintf("failed to marshal service config %+v: %v", sc, err))
}
return bs
}
type virtualHost struct {
// retry policy present in virtual host
retryConfig *xdsresource.RetryConfig
}
// routeCluster holds information about a cluster as referenced by a route.
type routeCluster struct {
name string // Name of the cluster.
interceptor httpfilter.ClientInterceptor // HTTP filters to run for RPCs matching this route.
}
type route struct {
m *xdsresource.CompositeMatcher // converted from route matchers
actionType xdsresource.RouteActionType // holds route action type
clusters wrr.WRR // holds *routeCluster entries
routeClusters []*grpcsync.RefCounted[*routeCluster] // Route clusters belonging to this route
maxStreamDuration time.Duration
retryConfig *xdsresource.RetryConfig
hashPolicies []*xdsresource.HashPolicy
autoHostRewrite bool
}
func (r route) String() string {
return fmt.Sprintf("%s -> { clusters: %v, maxStreamDuration: %v }", r.m.String(), r.clusters, r.maxStreamDuration)
}
// stoppableConfigSelector extends the iresolver.ConfigSelector interface with a
// stop() method. This makes it possible to swap the current config selector
// with an erroring config selector when the LDS or RDS resource is not found on
// the management server.
type stoppableConfigSelector interface {
iresolver.ConfigSelector
stop()
}
// erroringConfigSelector always returns an error, with the xDS node ID included
// in the error message. It is used to swap out the current config selector
// when the LDS or RDS resource is not found on the management server.
type erroringConfigSelector struct {
err error
}
func newErroringConfigSelector(err error, xdsNodeID string) *erroringConfigSelector {
return &erroringConfigSelector{err: annotateErrorWithNodeID(status.Error(codes.Unavailable, err.Error()), xdsNodeID)}
}
func (cs *erroringConfigSelector) SelectConfig(iresolver.RPCInfo) (*iresolver.RPCConfig, error) {
return nil, cs.err
}
func (cs *erroringConfigSelector) stop() {}
type configSelector struct {
channelID uint64 // Static hash when hash policy is HashPolicyTypeChannelID
xdsNodeID string // xDS node ID, for annotating errors.
sendNewServiceConfig func() // Function to send a new service config to gRPC.
// Configuration received from the xDS management server.
virtualHost virtualHost
routes []route
clusters map[string]*clusterInfo
plugins map[string]*clusterInfo
httpFilterConfig []xdsresource.HTTPFilter
xdsConfig *xdsresource.XDSConfig
}
var errNoMatchedRouteFound = status.Errorf(codes.Unavailable, "no matched route was found")
var errUnsupportedClientRouteAction = status.Errorf(codes.Unavailable, "matched route does not have a supported route action type")
// annotateErrorWithNodeID annotates the given error with the provided xDS node
// ID. This is used by the real config selector when it runs into errors, and
// also by the erroring config selector.
func annotateErrorWithNodeID(err error, nodeID string) error {
return fmt.Errorf("[xDS node id: %s]: %w", nodeID, err)
}
func (cs *configSelector) SelectConfig(rpcInfo iresolver.RPCInfo) (*iresolver.RPCConfig, error) {
var rt *route
md, _ := metadata.FromOutgoingContext(rpcInfo.Context)
if extraMD, ok := grpcutil.ExtraMetadata(rpcInfo.Context); ok {
md = metadata.Join(md, extraMD)
// Remove all binary headers. They are hard to match with. May need
// to add back if asked by users.
for k := range md {
if strings.HasSuffix(k, "-bin") {
delete(md, k)
}
}
}
// Loop through routes in order and select first match.
for _, r := range cs.routes {
if r.m.Match(rpcInfo.Method, md) {
rt = &r
break
}
}
if rt == nil || rt.clusters == nil {
return nil, annotateErrorWithNodeID(errNoMatchedRouteFound, cs.xdsNodeID)
}
if rt.actionType != xdsresource.RouteActionRoute {
return nil, annotateErrorWithNodeID(errUnsupportedClientRouteAction, cs.xdsNodeID)
}
rc, ok := rt.clusters.Next().(*grpcsync.RefCounted[*routeCluster])
if !ok {
return nil, annotateErrorWithNodeID(status.Errorf(codes.Internal, "error retrieving cluster for match: %v (%T)", rc, rc), cs.xdsNodeID)
}
cluster := rc.Value()
lbCtx := clustermanager.SetPickedCluster(rpcInfo.Context, cluster.name)
lbCtx = xdsresource.NewContextWithXDSConfig(lbCtx, cs.xdsConfig)
lbCtx = iringhash.SetXDSRequestHash(lbCtx, cs.generateHash(rpcInfo, rt.hashPolicies))
if rt.autoHostRewrite {
lbCtx = clusterimpl.EnableAutoHostRewrite(lbCtx)
}
config := &iresolver.RPCConfig{
Context: lbCtx,
Interceptor: cluster.interceptor,
}
// Add a ref to the selected cluster to keep the interceptors alive until RPC
// is committed.
rc.Increment()
if info, ok := cs.clusters[cluster.name]; ok {
// Add a ref to the selected cluster, as this RPC needs this
// cluster until it is committed.
info.refCount.Add(1)
config.OnCommitted = sync.OnceFunc(func() {
if v := info.refCount.Add(-1); v == 0 {
// We call unsubscribe rather than sendNewServiceConfig to
// prevent redundant updates. If the reference count in the
// dependency manager drops to zero, it will automatically
// trigger a service config update with this cluster
// removed. Calling unsubscribe allows the dependency
// manager to handle the update flow once and for all.
info.unsubscribe()
}
// Decrement the refcount of the route cluster and close the interceptor
// if refcount goes to zero.
rc.Decrement()
})
} else if info, ok := cs.plugins[cluster.name]; ok {
// Add a ref to the selected plugin, as this RPC needs this
// plugin until it is committed.
info.refCount.Add(1)
config.OnCommitted = sync.OnceFunc(func() {
if v := info.refCount.Add(-1); v == 0 {
// This entry will be removed from activePlugins when
// producing a new service config update.
cs.sendNewServiceConfig()
}
// Decrement the refcount of the route cluster and close the interceptor
// if refcount goes to zero.
rc.Decrement()
})
} else {
// This should be unreachable because all route clusters are normalized
// into cs.clusters or cs.plugins during config selector creation.
panic(fmt.Sprintf("matched cluster %q not found in ConfigSelector", cluster.name))
}
if rt.maxStreamDuration != 0 {
config.MethodConfig.Timeout = &rt.maxStreamDuration
}
if rt.retryConfig != nil {
config.MethodConfig.RetryPolicy = retryConfigToPolicy(rt.retryConfig)
} else if cs.virtualHost.retryConfig != nil {
config.MethodConfig.RetryPolicy = retryConfigToPolicy(cs.virtualHost.retryConfig)
}
return config, nil
}
func retryConfigToPolicy(config *xdsresource.RetryConfig) *serviceconfig.RetryPolicy {
return &serviceconfig.RetryPolicy{
MaxAttempts: int(config.NumRetries) + 1,
InitialBackoff: config.RetryBackoff.BaseInterval,
MaxBackoff: config.RetryBackoff.MaxInterval,
BackoffMultiplier: 2,
RetryableStatusCodes: config.RetryOn,
}
}
func (cs *configSelector) generateHash(rpcInfo iresolver.RPCInfo, hashPolicies []*xdsresource.HashPolicy) uint64 {
var hash uint64
var generatedHash bool
var md, emd metadata.MD
var mdRead bool
for _, policy := range hashPolicies {
var policyHash uint64
var generatedPolicyHash bool
switch policy.HashPolicyType {
case xdsresource.HashPolicyTypeHeader:
if strings.HasSuffix(policy.HeaderName, "-bin") {
continue
}
if !mdRead {
md, _ = metadata.FromOutgoingContext(rpcInfo.Context)
emd, _ = grpcutil.ExtraMetadata(rpcInfo.Context)
mdRead = true
}
values := emd.Get(policy.HeaderName)
if len(values) == 0 {
// Extra metadata (e.g. the "content-type" header) takes
// precedence over the user's metadata.
values = md.Get(policy.HeaderName)
if len(values) == 0 {
// If the header isn't present at all, this policy is a no-op.
continue
}
}
joinedValues := strings.Join(values, ",")
if policy.Regex != nil {
joinedValues = policy.Regex.ReplaceAllString(joinedValues, policy.RegexSubstitution)
}
policyHash = xxhash.Sum64String(joinedValues)
generatedHash = true
generatedPolicyHash = true
case xdsresource.HashPolicyTypeChannelID:
// Use the static channel ID as the hash for this policy.
policyHash = cs.channelID
generatedHash = true
generatedPolicyHash = true
}
// Deterministically combine the hash policies. Rotating prevents
// duplicate hash policies from cancelling each other out and preserves
// the 64 bits of entropy.
if generatedPolicyHash {
hash = bits.RotateLeft64(hash, 1)
hash = hash ^ policyHash
}
// If terminal policy and a hash has already been generated, ignore the
// rest of the policies and use that hash already generated.
if policy.Terminal && generatedHash {
break
}
}
if generatedHash {
return hash
}
// If no generated hash return a random long. In the grand scheme of things
// this logically will map to choosing a random backend to route request to.
return rand.Uint64()
}
// stop decrements refs of all clusters referenced by this config selector.
func (cs *configSelector) stop() {
// The resolver's old configSelector may be nil. Handle that here.
if cs == nil {
return
}
// Decrement the refcount of all the route clusters associated with this
// config selector and close the interceptors of the route cluster if it's
// refcount goes to zero.
for _, r := range cs.routes {
for _, rc := range r.routeClusters {
rc.Decrement()
}
}
// If any reference counts drop to zero, a service config update is required
// to remove the clusters. Since the old config selector is stopped
// after a new one is active, we must trigger a subsequent update to delete
// the now-unused clusters.
for _, ci := range cs.clusters {
if v := ci.refCount.Add(-1); v == 0 {
ci.unsubscribe()
}
}
for _, ci := range cs.plugins {
if v := ci.refCount.Add(-1); v == 0 {
cs.sendNewServiceConfig()
}
}
}

View File

@@ -0,0 +1,187 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package resolver
import (
"context"
"regexp"
"testing"
"time"
xxhash "github.com/cespare/xxhash/v2"
"github.com/google/go-cmp/cmp"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/grpcutil"
iresolver "google.golang.org/grpc/internal/resolver"
_ "google.golang.org/grpc/internal/xds/balancer/cdsbalancer" // To parse LB config
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
"google.golang.org/grpc/metadata"
)
var defaultTestTimeout = 10 * time.Second
type s struct {
grpctest.Tester
}
func Test(t *testing.T) {
grpctest.RunSubTests(t, s{})
}
func (s) TestPruneActiveClusters(t *testing.T) {
newClusterInfo := func(ref int32, unsubscribe func()) *clusterInfo {
ci := &clusterInfo{unsubscribe: unsubscribe}
ci.refCount.Store(ref)
return ci
}
r := &xdsResolver{
activeClusters: map[string]*clusterInfo{
"zero": newClusterInfo(0, func() {}),
"one": newClusterInfo(1, func() {}),
"two": newClusterInfo(2, func() {}),
"anotherzero": newClusterInfo(0, func() {}),
},
activePlugins: map[string]*clusterInfo{
"zero": newClusterInfo(0, nil),
"one": newClusterInfo(1, nil),
"two": newClusterInfo(2, nil),
"anotherzero": newClusterInfo(0, nil),
},
}
wantActiveClusters := map[string]int32{
"one": 1,
"two": 2,
}
wantActivePlugins := map[string]int32{
"one": 1,
"two": 2,
}
r.pruneActiveClustersAndPlugins()
getRefCounts := func(m map[string]*clusterInfo) map[string]int32 {
res := make(map[string]int32)
for k, v := range m {
res[k] = v.refCount.Load()
}
return res
}
if d := cmp.Diff(getRefCounts(r.activeClusters), wantActiveClusters); d != "" {
t.Fatalf("r.activeClusters refCounts mismatch (-got +want):\n%s", d)
}
if d := cmp.Diff(getRefCounts(r.activePlugins), wantActivePlugins); d != "" {
t.Fatalf("r.activePlugins refCounts mismatch (-got +want):\n%s", d)
}
}
func (s) TestGenerateRequestHash(t *testing.T) {
const channelID = 12378921
cs := &configSelector{channelID: channelID}
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
tests := []struct {
name string
hashPolicies []*xdsresource.HashPolicy
requestHashWant uint64
rpcInfo iresolver.RPCInfo
}{
// TestGenerateRequestHashHeaders tests generating request hashes for
// hash policies that specify to hash headers.
{
name: "test-generate-request-hash-headers",
hashPolicies: []*xdsresource.HashPolicy{{
HashPolicyType: xdsresource.HashPolicyTypeHeader,
HeaderName: ":path",
Regex: func() *regexp.Regexp { return regexp.MustCompile("/products") }(), // Will replace /products with /new-products, to test find and replace functionality.
RegexSubstitution: "/new-products",
}},
requestHashWant: xxhash.Sum64String("/new-products"),
rpcInfo: iresolver.RPCInfo{
Context: metadata.NewOutgoingContext(ctx, metadata.Pairs(":path", "/products")),
Method: "/some-method",
},
},
// TestGenerateHashChannelID tests generating request hashes for hash
// policies that specify to hash something that uniquely identifies the
// ClientConn (the pointer).
{
name: "test-generate-request-hash-channel-id",
hashPolicies: []*xdsresource.HashPolicy{{
HashPolicyType: xdsresource.HashPolicyTypeChannelID,
}},
requestHashWant: channelID,
rpcInfo: iresolver.RPCInfo{},
},
// TestGenerateRequestHashEmptyString tests generating request hashes
// for hash policies that specify to hash headers and replace empty
// strings in the headers.
{
name: "test-generate-request-hash-empty-string",
hashPolicies: []*xdsresource.HashPolicy{{
HashPolicyType: xdsresource.HashPolicyTypeHeader,
HeaderName: ":path",
Regex: func() *regexp.Regexp { return regexp.MustCompile("") }(),
RegexSubstitution: "e",
}},
requestHashWant: xxhash.Sum64String("eaebece"),
rpcInfo: iresolver.RPCInfo{
Context: metadata.NewOutgoingContext(ctx, metadata.Pairs(":path", "abc")),
Method: "/some-method",
},
},
// Tests that bin headers are skipped.
{
name: "skip-bin",
hashPolicies: []*xdsresource.HashPolicy{{
HashPolicyType: xdsresource.HashPolicyTypeHeader,
HeaderName: "something-bin",
}, {
HashPolicyType: xdsresource.HashPolicyTypeChannelID,
}},
requestHashWant: channelID,
rpcInfo: iresolver.RPCInfo{
Context: metadata.NewOutgoingContext(ctx, metadata.Pairs("something-bin", "xyz")),
},
},
// Tests that extra metadata takes precedence over the user's metadata.
{
name: "extra-metadata",
hashPolicies: []*xdsresource.HashPolicy{{
HashPolicyType: xdsresource.HashPolicyTypeHeader,
HeaderName: "content-type",
}},
requestHashWant: xxhash.Sum64String("grpc value"),
rpcInfo: iresolver.RPCInfo{
Context: grpcutil.WithExtraMetadata(
metadata.NewOutgoingContext(ctx, metadata.Pairs("content-type", "user value")),
metadata.Pairs("content-type", "grpc value"),
),
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
requestHashGot := cs.generateHash(test.rpcInfo, test.hashPolicies)
if requestHashGot != test.requestHashWant {
t.Fatalf("requestHashGot = %v, requestHashWant = %v", requestHashGot, test.requestHashWant)
}
})
}
}

View File

@@ -0,0 +1,187 @@
/*
*
* Copyright 2020 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package resolver_test
import (
"context"
"testing"
"github.com/envoyproxy/go-control-plane/pkg/wellknown"
"github.com/google/uuid"
"google.golang.org/grpc/internal/testutils"
"google.golang.org/grpc/internal/testutils/xds/e2e"
"google.golang.org/grpc/resolver"
"google.golang.org/protobuf/types/known/wrapperspb"
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
v3routerpb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/router/v3"
v3httppb "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3"
)
// Tests the case where the listener resource starts pointing to a new route
// configuration resource after the xDS resolver has successfully resolved the
// service name and pushed an update on the channel. The test verifies that the
// resolver stops requesting the old route configuration resource and requests
// the new resource, and once successfully resolved, verifies that the update
// from the resolver matches expected service config.
func (s) TestServiceWatch_ListenerPointsToNewRouteConfiguration(t *testing.T) {
// Spin up an xDS management server for the test.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
nodeID := uuid.New().String()
mgmtServer, lisCh, routeCfgCh, bc := setupManagementServerForTest(t, nodeID)
// Configure resources on the management server.
resources := e2e.DefaultClientResources(e2e.ResourceParams{
DialTarget: defaultTestServiceName,
NodeID: nodeID,
Host: defaultTestHostname,
Port: defaultTestPort[0],
SecLevel: e2e.SecurityLevelNone,
})
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)
// Verify initial update from the resolver.
waitForResourceNames(ctx, t, lisCh, []string{defaultTestServiceName})
waitForResourceNames(ctx, t, routeCfgCh, []string{resources.Routes[0].Name})
verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
// Update the listener resource to point to a new route configuration name.
// The old route configuration resource is left unchanged to prevent a race:
// if it were removed immediately, the resolver might encounter a
// "resource-not-found" error for the old route before the listener update
// successfully transitions the client to the new route.
newTestRouteConfigName := defaultTestRouteConfigName + "-new"
resources.Listeners = []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, newTestRouteConfigName)}
resources.SkipValidation = true
mgmtServer.Update(ctx, resources)
// Verify that the new route configuration resource is requested.
waitForResourceNames(ctx, t, routeCfgCh, []string{newTestRouteConfigName})
// Update the old route configuration resource by adding a new route.
resources.Routes[0].VirtualHosts[0].Routes = append(resources.Routes[0].VirtualHosts[0].Routes, &v3routepb.Route{
Match: &v3routepb.RouteMatch{
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/foo/bar"},
CaseSensitive: &wrapperspb.BoolValue{Value: false},
},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: "some-random-cluster"},
},
},
})
mgmtServer.Update(ctx, resources)
// Wait for no update from the resolver since the listener resource no
// longer points to the old route resource and new route resource hasn't
// been sent yet.
verifyNoUpdateFromResolver(ctx, t, stateCh)
// Update the management server with the new route configuration resource.
resources.Routes = append(resources.Routes, e2e.DefaultRouteConfig(newTestRouteConfigName, defaultTestServiceName, resources.Clusters[0].Name))
configureResources(ctx, t, mgmtServer, nodeID, resources.Listeners, resources.Routes, nil, nil)
// Ensure update from the resolver.
verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
}
// Tests the case where the listener resource changes to contain an inline route
// configuration and changes back to having a route configuration resource name.
// Verifies that the expected xDS resource names are requested by the resolver
// and that the update from the resolver matches expected service config.
func (s) TestServiceWatch_ListenerPointsToInlineRouteConfiguration(t *testing.T) {
// Spin up an xDS management server for the test.
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
nodeID := uuid.New().String()
mgmtServer, lisCh, routeCfgCh, bc := setupManagementServerForTest(t, nodeID)
// Configure resources on the management server.
resources := e2e.DefaultClientResources(e2e.ResourceParams{
DialTarget: defaultTestServiceName,
NodeID: nodeID,
Host: defaultTestHostname,
Port: defaultTestPort[0],
SecLevel: e2e.SecurityLevelNone,
})
if err := mgmtServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
stateCh, _, _ := buildResolverForTarget(t, resolver.Target{URL: *testutils.MustParseURL("xds:///" + defaultTestServiceName)}, bc)
// Verify initial update from the resolver.
waitForResourceNames(ctx, t, lisCh, []string{defaultTestServiceName})
waitForResourceNames(ctx, t, routeCfgCh, []string{resources.Routes[0].Name})
verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
// Update listener to contain an inline route configuration.
hcm := testutils.MarshalAny(t, &v3httppb.HttpConnectionManager{
RouteSpecifier: &v3httppb.HttpConnectionManager_RouteConfig{
RouteConfig: &v3routepb.RouteConfiguration{
Name: resources.Routes[0].Name,
VirtualHosts: []*v3routepb.VirtualHost{{
Domains: []string{defaultTestServiceName},
Routes: []*v3routepb.Route{{
Match: &v3routepb.RouteMatch{
PathSpecifier: &v3routepb.RouteMatch_Prefix{Prefix: "/"},
},
Action: &v3routepb.Route_Route{
Route: &v3routepb.RouteAction{
ClusterSpecifier: &v3routepb.RouteAction_Cluster{Cluster: resources.Clusters[0].Name},
},
},
}},
}},
},
},
HttpFilters: []*v3httppb.HttpFilter{e2e.HTTPFilter("router", &v3routerpb.Router{})},
})
resources.Listeners = []*v3listenerpb.Listener{{
Name: defaultTestServiceName,
ApiListener: &v3listenerpb.ApiListener{ApiListener: hcm},
FilterChains: []*v3listenerpb.FilterChain{{
Name: "filter-chain-name",
Filters: []*v3listenerpb.Filter{{
Name: wellknown.HTTPConnectionManager,
ConfigType: &v3listenerpb.Filter_TypedConfig{TypedConfig: hcm},
}},
}},
}}
configureResources(ctx, t, mgmtServer, nodeID, resources.Listeners, nil, nil, nil)
// Verify that the old route configuration is not requested anymore.
waitForResourceNames(ctx, t, routeCfgCh, []string{})
verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
// Update listener back to contain a route configuration name.
resources.Listeners = []*v3listenerpb.Listener{e2e.DefaultClientListener(defaultTestServiceName, resources.Routes[0].Name)}
configureResources(ctx, t, mgmtServer, nodeID, resources.Listeners, resources.Routes, nil, nil)
// Verify that that route configuration resource is requested.
waitForResourceNames(ctx, t, routeCfgCh, []string{resources.Routes[0].Name})
// Verify that appropriate SC is pushed on the channel.
verifyUpdateFromResolver(ctx, t, stateCh, wantServiceConfig(resources.Clusters[0].Name))
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,731 @@
/*
* Copyright 2019 gRPC authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
// Package resolver implements the xds resolver, that does LDS and RDS to find
// the cluster to use.
package resolver
import (
"context"
"fmt"
rand "math/rand/v2"
"slices"
"strings"
"sync/atomic"
"google.golang.org/grpc"
estats "google.golang.org/grpc/experimental/stats"
"google.golang.org/grpc/internal"
"google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/grpcsync"
iresolver "google.golang.org/grpc/internal/resolver"
"google.golang.org/grpc/internal/wrr"
"google.golang.org/grpc/internal/xds/bootstrap"
"google.golang.org/grpc/internal/xds/httpfilter"
rinternal "google.golang.org/grpc/internal/xds/resolver/internal"
"google.golang.org/grpc/internal/xds/xdsclient"
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
"google.golang.org/grpc/internal/xds/xdsdepmgr"
"google.golang.org/grpc/resolver"
)
// Scheme is the xDS resolver's scheme.
//
// TODO(easwars): Rename this package as xdsresolver so that this is accessed as
// xdsresolver.Scheme
const Scheme = "xds"
// newBuilderWithConfigForTesting creates a new xds resolver builder using a
// specific xds bootstrap config, so tests can use multiple xDS clients in
// different ClientConns at the same time. The builder creates a new pool with
// the provided config and a new xDS client in that pool.
func newBuilderWithConfigForTesting(config []byte) (resolver.Builder, error) {
return &xdsResolverBuilder{
newXDSClient: func(name string, mr estats.MetricsRecorder) (xdsclient.XDSClient, func(), error) {
config, err := bootstrap.NewConfigFromContents(config)
if err != nil {
return nil, nil, err
}
pool := xdsclient.NewPool(config)
return pool.NewClientForTesting(xdsclient.OptionsForTesting{
Name: name,
MetricsRecorder: mr,
})
},
}, nil
}
// newBuilderWithPoolForTesting creates a new xds resolver builder using the
// specific xds client pool, so that tests have complete control over the exact
// specific xds client pool being used.
func newBuilderWithPoolForTesting(pool *xdsclient.Pool) (resolver.Builder, error) {
return &xdsResolverBuilder{
newXDSClient: func(name string, mr estats.MetricsRecorder) (xdsclient.XDSClient, func(), error) {
return pool.NewClientForTesting(xdsclient.OptionsForTesting{
Name: name,
MetricsRecorder: mr,
})
},
}, nil
}
// newBuilderWithClientForTesting creates a new xds resolver builder using the
// specific xDS client, so that tests have complete control over the exact
// specific xDS client being used.
func newBuilderWithClientForTesting(client xdsclient.XDSClient) (resolver.Builder, error) {
return &xdsResolverBuilder{
newXDSClient: func(string, estats.MetricsRecorder) (xdsclient.XDSClient, func(), error) {
// Returning an empty close func here means that the responsibility
// of closing the client lies with the caller.
return client, func() {}, nil
},
}, nil
}
func init() {
resolver.Register(&xdsResolverBuilder{})
internal.NewXDSResolverWithConfigForTesting = newBuilderWithConfigForTesting
internal.NewXDSResolverWithPoolForTesting = newBuilderWithPoolForTesting
internal.NewXDSResolverWithClientForTesting = newBuilderWithClientForTesting
rinternal.NewWRR = wrr.NewRandom
rinternal.NewXDSClient = xdsclient.DefaultPool.NewClient
}
type xdsResolverBuilder struct {
newXDSClient func(string, estats.MetricsRecorder) (xdsclient.XDSClient, func(), error)
}
// Build helps implement the resolver.Builder interface.
//
// The xds bootstrap process is performed (and a new xDS client is built) every
// time an xds resolver is built.
func (b *xdsResolverBuilder) Build(target resolver.Target, cc resolver.ClientConn, opts resolver.BuildOptions) (_ resolver.Resolver, retErr error) {
// Initialize the xDS client.
newXDSClient := rinternal.NewXDSClient.(func(string, estats.MetricsRecorder) (xdsclient.XDSClient, func(), error))
if b.newXDSClient != nil {
newXDSClient = b.newXDSClient
}
client, xdsClientClose, err := newXDSClient(target.String(), opts.MetricsRecorder)
if err != nil {
return nil, fmt.Errorf("xds: failed to create xds-client: %v", err)
}
template, err := sanityChecksOnBootstrapConfig(target, client)
if err != nil {
xdsClientClose()
return nil, err
}
ldsResourceName := bootstrap.PopulateResourceTemplate(template, target.Endpoint())
ctx, cancel := context.WithCancel(context.Background())
r := &xdsResolver{
cc: cc,
xdsClient: client,
xdsClientClose: xdsClientClose,
activeClusters: make(map[string]*clusterInfo),
activePlugins: make(map[string]*clusterInfo),
httpFilters: make(map[clientFilterKey]httpfilter.ClientFilter),
channelID: rand.Uint64(),
ldsResourceName: ldsResourceName,
target: target.String(),
metricsRecorder: opts.MetricsRecorder,
// serializer used to synchronize the following:
// - updates from the dependency manager. This could lead to generation
// of new service config if resolution is complete.
// - completion of an RPC to a removed cluster causing the associated
// ref count to become zero, resulting in generation of new service
// config.
// - stopping of a config selector that results in generation of new
// service config.
serializer: grpcsync.NewCallbackSerializer(ctx),
serializerCancel: cancel,
}
r.logger = prefixLogger(r)
r.logger.Infof("Creating resolver for target: %+v", target)
dmSet := make(chan struct{})
// Schedule a callback that blocks until r.dm is set i.e xdsdepmgr.New()
// returns. This acts as a gatekeeper: even if dependency manager sends the
// updates before the xdsdepmgr.New() has a chance to return, they will be
// queued behind this blocker and processed only after initialization is
// complete.
r.serializer.TrySchedule(func(ctx context.Context) {
select {
case <-dmSet:
case <-ctx.Done():
}
})
r.dm = xdsdepmgr.New(r.ldsResourceName, opts.Authority, r.xdsClient, r)
close(dmSet)
return r, nil
}
// Performs the following sanity checks:
// - Verifies that the bootstrap configuration is not empty.
// - Verifies that if xDS credentials are specified by the user, the
// bootstrap configuration contains certificate providers.
// - Verifies that if the provided dial target contains an authority, the
// bootstrap configuration contains server config for that authority.
//
// Returns the listener resource name template to use. If any of the above
// validations fail, a non-nil error is returned.
func sanityChecksOnBootstrapConfig(target resolver.Target, client xdsclient.XDSClient) (string, error) {
bootstrapConfig := client.BootstrapConfig()
if bootstrapConfig == nil {
// This is never expected to happen after a successful xDS client
// creation. Defensive programming.
return "", fmt.Errorf("xds: bootstrap configuration is empty")
}
// Find the client listener template to use from the bootstrap config:
// - If authority is not set in the target, use the top level template
// - If authority is set, use the template from the authority map.
template := bootstrapConfig.ClientDefaultListenerResourceNameTemplate()
if authority := target.URL.Host; authority != "" {
authorities := bootstrapConfig.Authorities()
if authorities == nil {
return "", fmt.Errorf("xds: authority %q specified in dial target %q is not found in the bootstrap file", authority, target)
}
a := authorities[authority]
if a == nil {
return "", fmt.Errorf("xds: authority %q specified in dial target %q is not found in the bootstrap file", authority, target)
}
if a.ClientListenerResourceNameTemplate != "" {
// This check will never be false, because
// ClientListenerResourceNameTemplate is required to start with
// xdstp://, and has a default value (not an empty string) if unset.
template = a.ClientListenerResourceNameTemplate
}
}
return template, nil
}
// Name helps implement the resolver.Builder interface.
func (*xdsResolverBuilder) Scheme() string {
return Scheme
}
// xdsResolver implements the resolver.Resolver interface.
//
// It manages the dependency manager which in turn manages all xDS resource
// watches. It receives the xDS resource config and passes them to ClientConn.
type xdsResolver struct {
// The following fields are initialized at creation time and are read-only
// after that.
cc resolver.ClientConn
logger *grpclog.PrefixLogger
ldsResourceName string
dm *xdsdepmgr.DependencyManager
xdsClient xdsclient.XDSClient
xdsClientClose func()
channelID uint64 // Unique random ID for the channel owning this resolver.
target string
metricsRecorder estats.MetricsRecorder
// All methods on the xdsResolver type except for the ones invoked by gRPC,
// i.e ResolveNow() and Close(), are guaranteed to execute in the context of
// this serializer's callback. We use the serializer because these shared
// states are accessed by each RPC when it is committed, and so
// serializer is preffered over a mutex.
serializer *grpcsync.CallbackSerializer
serializerCancel context.CancelFunc
// The following fields are accessed only from within the serializer
// callbacks.
xdsConfig *xdsresource.XDSConfig
// activeClusters is a map from cluster name to information about the
// weighted cluster that includes a reference count and load balancing
// configuration. These counts are used only by the resolver. The current
// configSelector holds one reference, and each ongoing RPC holds an
// additional reference. When the count hits zero, the resolver removes the
// cluster from this map and calls unsubscribe. This signals the dependency
// manager to stop the xDS watch once its own reference count reaches zero.
activeClusters map[string]*clusterInfo
// activePlugins is a map from cluster specifier plugin name to information
// about the cluster specifier plugin that includes a ref count and load
// balancing configuration. These counts are used only by the resolver. The
// current configSelector holds one reference, and each ongoing RPC holds an
// additional reference. When the count hits zero, the resolver removes the
// plugin name from this map.
activePlugins map[string]*clusterInfo
curConfigSelector stoppableConfigSelector
// httpFilters is a map from client filter key to client filter instance. It
// lives here so that the resolver can reuse filter instances across config
// updates when the same filter is specified, and to be able to clean up
// filter instances that are no longer used.
httpFilters map[clientFilterKey]httpfilter.ClientFilter
}
// ResolveNow calls RequestDNSReresolution on the dependency manager.
func (r *xdsResolver) ResolveNow(opts resolver.ResolveNowOptions) {
if r.dm != nil {
r.dm.RequestDNSReresolution(opts)
}
}
func (r *xdsResolver) Close() {
// Cancel the context passed to the serializer and wait for any scheduled
// callbacks to complete. Canceling the context ensures that no new
// callbacks will be scheduled.
r.serializerCancel()
<-r.serializer.Done()
if r.dm != nil {
r.dm.Close()
}
if r.curConfigSelector != nil {
r.curConfigSelector.stop()
}
for _, cf := range r.httpFilters {
cf.Close()
}
// Release the xDS client only after the filters are torn down: filters
// may hold side channels whose credentials are owned by the client's
// bootstrap config and are released when the last reference to the
// client is dropped.
if r.xdsClientClose != nil {
r.xdsClientClose()
}
// Drop field references so the resolver isn't retained across ClientConn
// close via any cross-object retention chain (e.g. ClientConn holding the
// ServiceConfig that captures the configSelector, shared xDS-client
// watcher metadata, channelz trace entries). Each nil individually looks
// harmless; the combination breaks the multi-path retention observed when
// callers repeatedly Close and redial DirectPath channels.
r.cc = nil
r.curConfigSelector = nil
r.dm = nil
r.xdsClient = nil
r.xdsClientClose = nil
r.httpFilters = nil
r.activeClusters = nil
r.activePlugins = nil
r.xdsConfig = nil
r.logger.Infof("Shutdown")
}
// Update is called when there is a new xDS config available from the dependency
// manager and does the following:
// - creates a new config selector (this involves incrementing references to
// clusters owned by this config selector).
// - stops the old config selector (this involves decrementing references to
// clusters owned by this config selector).
// - prunes active clusters and pushes a new service config to the channel.
// - updates the current config selector used by the resolver.
func (r *xdsResolver) Update(config *xdsresource.XDSConfig) {
r.serializer.TrySchedule(func(context.Context) {
r.xdsConfig = config
cs, err := r.newConfigSelector()
if err != nil {
r.onResourceError(err)
return
}
r.sendNewServiceConfig(cs)
if r.curConfigSelector != nil {
r.curConfigSelector.stop()
}
r.curConfigSelector = cs
})
}
func (r *xdsResolver) Error(err error) {
r.serializer.TrySchedule(func(context.Context) {
r.onResourceError(err)
})
}
// sendNewServiceConfig prunes active clusters, generates a new service config
// based on the current set of active clusters, and sends an update to the
// channel with that service config and the provided config selector. It is safe
// to ignore the error from `cc.UpdateState` because the clientconn uses the
// given configSelector even if it returns an error.
//
// Only executed in the context of a serializer callback.
func (r *xdsResolver) sendNewServiceConfig(cs stoppableConfigSelector) {
// Delete entries from r.activeClusters with zero references;
// otherwise serviceConfigJSON will generate a config including
// them.
r.pruneActiveClustersAndPlugins()
if errCS, ok := cs.(*erroringConfigSelector); ok {
// Send an empty config, which picks pick-first, with no address, and
// puts the ClientConn into transient failure.
//
// This call to UpdateState is expected to return ErrBadResolverState
// since pick_first doesn't like an update with no addresses.
r.cc.UpdateState(resolver.State{ServiceConfig: r.cc.ParseServiceConfig("{}")})
// Send a resolver error to pick_first so that RPCs will fail with a
// more meaningful error, as opposed to one that says that pick_first
// received no addresses.
r.cc.ReportError(errCS.err)
return
}
sc := serviceConfigJSON(r.activeClusters, r.activePlugins)
if r.logger.V(2) {
r.logger.Infof("For Listener resource %q and RouteConfiguration resource %q, generated service config: %s", r.ldsResourceName, r.xdsConfig.Listener.APIListener.RouteConfigName, string(sc))
}
// Send the update to the ClientConn.
state := iresolver.SetConfigSelector(resolver.State{
ServiceConfig: r.cc.ParseServiceConfig(string(sc)),
}, cs)
state = xdsresource.SetXDSConfig(state, r.xdsConfig)
state = xdsdepmgr.SetXDSClusterSubscriber(state, r.dm)
r.cc.UpdateState(xdsclient.SetClient(state, r.xdsClient))
}
// newConfigSelector creates a new config selector using the most recently
// received listener and route config updates. May add entries to
// r.activeClusters for previously-unseen clusters.
//
// Only executed in the context of a serializer callback.
func (r *xdsResolver) newConfigSelector() (_ *configSelector, err error) {
cs := &configSelector{
channelID: r.channelID,
xdsNodeID: r.xdsClient.BootstrapConfig().Node().GetId(),
sendNewServiceConfig: func() {
r.serializer.TrySchedule(func(context.Context) {
r.sendNewServiceConfig(r.curConfigSelector)
})
},
virtualHost: virtualHost{
retryConfig: r.xdsConfig.VirtualHost.RetryConfig,
},
routes: make([]route, len(r.xdsConfig.VirtualHost.Routes)),
clusters: make(map[string]*clusterInfo),
plugins: make(map[string]*clusterInfo),
httpFilterConfig: r.xdsConfig.Listener.APIListener.HTTPFilters,
xdsConfig: r.xdsConfig,
}
defer func() {
if err != nil {
// Stop the config selector if an error occurs during construction
// to ensure that interceptors that were created successfully before
// the error are cleaned up.
cs.stop()
}
}()
for i, rt := range r.xdsConfig.VirtualHost.Routes {
clusters := rinternal.NewWRR.(func() wrr.WRR)()
interceptors := []httpfilter.ClientInterceptor{}
// TODO: Carve out the common logic between the ClusterSpecifierPlugin
// and WeightedClusters.
if rt.ClusterSpecifierPlugin != "" {
clusterName := clusterSpecifierPluginPrefix + rt.ClusterSpecifierPlugin
interceptor, err := r.newInterceptor(r.xdsConfig.Listener.APIListener.HTTPFilters, nil, rt.HTTPFilterConfigOverride, r.xdsConfig.VirtualHost.HTTPFilterConfigOverride)
if err != nil {
// Clean up any interceptors that were successfully built
// for the current route before this error occurred. Note
// that this is not handled by the call to cs.stop() in the
// deferred function.
for _, i := range interceptors {
i.Close()
}
return nil, err
}
routeCluster := &routeCluster{
name: clusterName,
interceptor: interceptor,
}
rc := grpcsync.NewRefCounted(routeCluster, func() { interceptor.Close() })
cs.routes[i].routeClusters = append(cs.routes[i].routeClusters, rc)
clusters.Add(rc, 1)
ci := r.addOrGetActiveClusterInfo(clusterName, "")
ci.cfg = xdsChildConfig{ChildPolicy: balancerConfig(r.xdsConfig.RouteConfig.ClusterSpecifierPlugins[rt.ClusterSpecifierPlugin])}
cs.plugins[clusterName] = ci
} else {
for _, wc := range rt.WeightedClusters {
clusterName := clusterPrefix + wc.Name
interceptor, err := r.newInterceptor(r.xdsConfig.Listener.APIListener.HTTPFilters, wc.HTTPFilterConfigOverride, rt.HTTPFilterConfigOverride, r.xdsConfig.VirtualHost.HTTPFilterConfigOverride)
if err != nil {
// Clean up any interceptors that were successfully built
// for the current route before this error occurred. Note
// that this is not handled by the call to cs.stop() in the
// deferred function.
for _, i := range interceptors {
i.Close()
}
return nil, err
}
routeCluster := &routeCluster{
name: clusterName,
interceptor: interceptor,
}
rc := grpcsync.NewRefCounted(routeCluster, func() { interceptor.Close() })
cs.routes[i].routeClusters = append(cs.routes[i].routeClusters, rc)
clusters.Add(rc, int64(wc.Weight))
ci := r.addOrGetActiveClusterInfo(clusterName, wc.Name)
ci.cfg = xdsChildConfig{ChildPolicy: newBalancerConfig(cdsName, cdsBalancerConfig{Cluster: wc.Name})}
cs.clusters[clusterName] = ci
}
}
cs.routes[i].clusters = clusters
cs.routes[i].m = xdsresource.RouteToMatcher(rt)
cs.routes[i].actionType = rt.ActionType
if rt.MaxStreamDuration == nil {
cs.routes[i].maxStreamDuration = r.xdsConfig.Listener.APIListener.MaxStreamDuration
} else {
cs.routes[i].maxStreamDuration = *rt.MaxStreamDuration
}
cs.routes[i].retryConfig = rt.RetryConfig
cs.routes[i].hashPolicies = rt.HashPolicies
cs.routes[i].autoHostRewrite = rt.AutoHostRewrite
}
// Account for this config selector's clusters. Do this after no further
// errors may occur. Note: cs.clusters are pointers to entries in
// activeClusters.
for _, ci := range cs.clusters {
ci.refCount.Add(1)
}
for _, ci := range cs.plugins {
ci.refCount.Add(1)
}
// Cleanup filter instances that are no longer specified in the current
// listener resource.
filtersInNewConfig := make(map[clientFilterKey]bool)
for _, filter := range r.xdsConfig.Listener.APIListener.HTTPFilters {
filtersInNewConfig[newClientFilterKey(&filter)] = true
}
for key, cf := range r.httpFilters {
if _, ok := filtersInNewConfig[key]; ok {
continue
}
cf.Close()
delete(r.httpFilters, key)
}
return cs, nil
}
// pruneActiveClustersAndPlugins removes entries from activeClusters and
// activePlugins that have a reference count of zero. For clusters, it also
// invokes the unsubscribe function to signal the dependency manager to stop the
// xDS watch. Because cluster specifier plugins do not have their own watches,
// they are simply removed from the map without an unsubscribe call.
//
// Only executed in the context of a serializer callback.
func (r *xdsResolver) pruneActiveClustersAndPlugins() {
for cluster, ci := range r.activeClusters {
if ci.refCount.Load() == 0 {
ci.unsubscribe()
delete(r.activeClusters, cluster)
}
}
for cluster, ci := range r.activePlugins {
if ci.refCount.Load() == 0 {
delete(r.activePlugins, cluster)
}
}
}
// addOrGetActiveClusterInfo returns the clusterInfo for the provided key,
// creating it if it does not exist. It accepts the following parameters:
// - key: Formatted as "cluster:<name>" or "cluster_specifier_plugin:<name>",
// this is the lookup key for the activeClusters or activePlugins maps.
// - name: The actual xDS resource name used to initiate a CDS watch.
// If empty (e.g., for plugins), no resource watch is triggered.
//
// This function manages entry creation and xDS subscriptions but does not
// increment the reference count of the returned clusterInfo.
func (r *xdsResolver) addOrGetActiveClusterInfo(key string, name string) *clusterInfo {
if name == "" {
ci, ok := r.activePlugins[key]
if !ok {
ci = &clusterInfo{}
r.activePlugins[key] = ci
}
return ci
}
ci, ok := r.activeClusters[key]
if !ok {
ci = &clusterInfo{unsubscribe: r.dm.SubscribeToCluster(name)}
r.activeClusters[key] = ci
}
return ci
}
type clusterInfo struct {
// refCount is the number of references to this cluster.
refCount atomic.Int32
// cfg is the child configuration for this cluster, containing either the
// csp config or the cds cluster config.
cfg xdsChildConfig
// unsubscribe is the function to call to unsubscribe from this cluster's
// CDS resource. It is populated only for clusters in activeClusters and not
// for cluster specifier plugins. When invoked, it decrements the reference
// count in the dependency manager; once that count reaches zero, the
// underlying CDS watch is terminated. Plugins do not have associated
// watches and therefore do not require an unsubscribe function.
unsubscribe func()
}
// Contains common functionality to be executed when resources of either type
// are removed.
//
// Only executed in the context of a serializer callback.
func (r *xdsResolver) onResourceError(err error) {
// We cannot remove clusters from the service config that have ongoing RPCs.
// Instead, what we can do is to send an erroring config selector
// along with normal service config. This will ensure that new RPCs will
// fail, and once the active RPCs complete, the reference counts on the
// clusters will come down to zero. At that point, we will send an empty
// service config with no addresses. This results in the pick-first
// LB policy being configured on the channel, and since there are no
// address, pick-first will put the channel in TRANSIENT_FAILURE.
cs := newErroringConfigSelector(err, r.xdsClient.BootstrapConfig().Node().GetId())
r.sendNewServiceConfig(cs)
// Stop and dereference the active config selector, if one exists.
if r.curConfigSelector != nil {
r.curConfigSelector.stop()
}
r.curConfigSelector = cs
}
// newInterceptor builds a chain of client interceptors for the given filters
// and override configuration. The cluster override has the highest priority,
// followed by the route override, and finally the virtual host override.
//
// Only executed in the context of a serializer callback.
func (r *xdsResolver) newInterceptor(filters []xdsresource.HTTPFilter, clusterOverride, routeOverride, virtualHostOverride map[string]httpfilter.FilterConfig) (_ httpfilter.ClientInterceptor, err error) {
interceptors := make([]httpfilter.ClientInterceptor, 0, len(filters))
defer func() {
// Clean up any interceptors that were successfully built before the
// error occurred, to avoid leaking resources.
if err != nil {
for _, i := range interceptors {
i.Close()
}
}
}()
for _, filter := range filters {
override := clusterOverride[filter.Name]
if override == nil {
override = routeOverride[filter.Name]
}
if override == nil {
override = virtualHostOverride[filter.Name]
}
// Determine the effective disabled state of the filter. The base
// configuration's disabled state is used unless an override is present.
// If an override is present, the filter is disabled if the override is
// a DisabledFilterConfig.
disabled := filter.Disabled
if override != nil {
_, disabled = override.(httpfilter.DisabledFilterConfig)
}
if disabled {
if r.logger.V(2) {
r.logger.Infof("Filter %q has been disabled.", filter.Name)
}
continue
}
builder, ok := filter.Filter.(httpfilter.ClientFilterBuilder)
if !ok {
// Should not happen if it passed xdsClient validation.
return nil, fmt.Errorf("filter %q does not support use in client", filter.Name)
}
clientFilter := r.getOrCreateClientFilter(builder, newClientFilterKey(&filter))
i, err := clientFilter.BuildClientInterceptor(filter.Config, override)
if err != nil {
return nil, fmt.Errorf("failed to build client interceptor for filter %q: %v", filter.Name, err)
}
if i != nil {
interceptors = append(interceptors, i)
}
}
return &interceptorList{interceptors: interceptors}, nil
}
// interceptorList is a client interceptor that contains a list of client
// interceptors to execute in order.
type interceptorList struct {
interceptors []httpfilter.ClientInterceptor
}
func (il *interceptorList) NewStream(ctx context.Context, ri iresolver.RPCInfo, newStream func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStream, error), opts ...grpc.CallOption) (grpc.ClientStream, error) {
for idx := len(il.interceptors) - 1; idx >= 0; idx-- {
ns := newStream
i := il.interceptors[idx]
newStream = func(ctx context.Context, opts ...grpc.CallOption) (grpc.ClientStream, error) {
return i.NewStream(ctx, ri, ns, opts...)
}
}
return newStream(ctx, opts...)
}
func (il *interceptorList) Close() {
for _, i := range il.interceptors {
i.Close()
}
}
// getOrCreateClientFilter retrieves an existing client filter from the
// httpFilters map or creates a new one if it doesn't exist. It uses the filter
// builder to create a new client filter and stores it in the httpFilters map
// for future use.
//
// Only executed in the context of a serializer callback.
func (r *xdsResolver) getOrCreateClientFilter(builder httpfilter.ClientFilterBuilder, key clientFilterKey) httpfilter.ClientFilter {
clientFilter, ok := r.httpFilters[key]
if ok {
return clientFilter
}
cf := builder.BuildClientFilter(httpfilter.ClientFilterOptions{
FilterName: key.name,
MetricsRecorder: r.metricsRecorder,
Target: r.target,
})
r.httpFilters[key] = cf
return cf
}
// newClientFilterKey generates a key for the given filter using the filter name
// and type URLs. This is used for storing ClientFilters in a map.
func newClientFilterKey(f *xdsresource.HTTPFilter) clientFilterKey {
typeURLs := slices.Clone(f.Filter.TypeURLs())
slices.Sort(typeURLs)
return clientFilterKey{
name: f.Name,
typeURLs: strings.Join(typeURLs, ":"),
}
}
func (f *clientFilterKey) String() string {
return f.name + ":" + f.typeURLs
}
type clientFilterKey struct {
name string
typeURLs string
}

File diff suppressed because it is too large Load Diff