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
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
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
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
Stale bot / stale (push) Failing after 1s
Some checks failed
codecov / upload (push) Failing after 13m59s
CodeQL / Analyze (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
Testing / tests (arm, latest) (push) Waiting to run
Testing / vet-proto (push) Failing after 1s
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
Stale bot / stale (push) Failing after 1s
This commit is contained in:
932
internal/xds/balancer/cdsbalancer/aggregate_cluster_test.go
Normal file
932
internal/xds/balancer/cdsbalancer/aggregate_cluster_test.go
Normal file
@@ -0,0 +1,932 @@
|
||||
/*
|
||||
* 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 cdsbalancer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/internal/pretty"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/xds/balancer/priority"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/resolver/manual"
|
||||
"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"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
|
||||
_ "google.golang.org/grpc/balancer/pickfirst"
|
||||
_ "google.golang.org/grpc/internal/xds/httpfilter/router" // Register the router filter.
|
||||
_ "google.golang.org/grpc/internal/xds/resolver" // Register the xds resolver
|
||||
)
|
||||
|
||||
// makeAggregateClusterResource returns an aggregate cluster resource with the
|
||||
// given name and list of child names.
|
||||
func makeAggregateClusterResource(name string, childNames []string) *v3clusterpb.Cluster {
|
||||
return e2e.ClusterResourceWithOptions(e2e.ClusterOptions{
|
||||
ClusterName: name,
|
||||
Type: e2e.ClusterTypeAggregate,
|
||||
ChildNames: childNames,
|
||||
})
|
||||
}
|
||||
|
||||
// makeLogicalDNSClusterResource returns a LOGICAL_DNS cluster resource with the
|
||||
// given name and given DNS host and port.
|
||||
func makeLogicalDNSClusterResource(name, dnsHost string, dnsPort uint32) *v3clusterpb.Cluster {
|
||||
return e2e.ClusterResourceWithOptions(e2e.ClusterOptions{
|
||||
ClusterName: name,
|
||||
Type: e2e.ClusterTypeLogicalDNS,
|
||||
DNSHostName: dnsHost,
|
||||
DNSPort: dnsPort,
|
||||
})
|
||||
}
|
||||
|
||||
// verifyDNSResolution verifies that the DNS resolver is started for the expected target.
|
||||
func verifyDNSResolution(ctx context.Context, t *testing.T, dnsTargetCh chan resolver.Target, dnsR *manual.Resolver, host string, port uint32) {
|
||||
t.Helper()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout waiting for DNS watch")
|
||||
case target := <-dnsTargetCh:
|
||||
addr := fmt.Sprintf("%s:%d", host, port)
|
||||
if target.Endpoint() != addr {
|
||||
t.Fatalf("DNS resolution started for target %q, want %q", target.Endpoint(), addr)
|
||||
}
|
||||
dnsR.UpdateState(resolver.State{
|
||||
Endpoints: []resolver.Endpoint{{Addresses: []resolver.Address{{Addr: addr}}}},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the case where the cluster resource requested is a leaf cluster. The
|
||||
// management server sends two updates for the same leaf cluster resource. The
|
||||
// test verifies that the load balancing configuration pushed to the priority LB
|
||||
// policy contains the expected discovery mechanism corresponding to the leaf
|
||||
// cluster, on both occasions.
|
||||
func (s) TestAggregateClusterSuccess_LeafNode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
firstClusterResource *v3clusterpb.Cluster
|
||||
secondClusterResource *v3clusterpb.Cluster
|
||||
wantFirstChildCfg serviceconfig.LoadBalancingConfig
|
||||
wantSecondChildCfg serviceconfig.LoadBalancingConfig
|
||||
}{
|
||||
{
|
||||
name: "eds",
|
||||
firstClusterResource: e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone),
|
||||
secondClusterResource: e2e.DefaultCluster(clusterName, serviceName+"-new", e2e.SecurityLevelNone),
|
||||
wantFirstChildCfg: &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(clusterName),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
},
|
||||
Priorities: []string{"priority-0-0"},
|
||||
},
|
||||
wantSecondChildCfg: &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-1-0": {
|
||||
Config: createPriorityConfig(clusterName),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
},
|
||||
Priorities: []string{"priority-1-0"},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "dns",
|
||||
firstClusterResource: makeLogicalDNSClusterResource(clusterName, "dns_host", uint32(port)),
|
||||
secondClusterResource: makeLogicalDNSClusterResource(clusterName, "dns_host_new", uint32(port)),
|
||||
wantFirstChildCfg: &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{"priority-0": {Config: createPriorityConfig(clusterName)}},
|
||||
Priorities: []string{"priority-0"},
|
||||
},
|
||||
wantSecondChildCfg: &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{"priority-1": {Config: createPriorityConfig(clusterName)}},
|
||||
Priorities: []string{"priority-1"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
|
||||
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
|
||||
|
||||
// Push the first cluster resource through the management server and
|
||||
// verify the configuration pushed to the child policy.
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{test.firstClusterResource},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
|
||||
SkipValidation: true,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, test.wantFirstChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Push the second cluster resource through the management server and
|
||||
// verify the configuration pushed to the child policy.
|
||||
resources.Clusters[0] = test.secondClusterResource
|
||||
resources.Endpoints[0] = e2e.DefaultEndpoint(serviceName+"-new", host, []uint32{port})
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, test.wantSecondChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the case where the cluster resource requested is an aggregate cluster
|
||||
// root pointing to two child clusters, one of type EDS and the other of type
|
||||
// LogicalDNS. The test verifies that load balancing configuration is pushed to
|
||||
// the priority LB policy only when all child clusters are resolved and it also
|
||||
// verifies that the pushed configuration contains the expected priority config.
|
||||
// The test then updates the aggregate cluster to point to two child clusters,
|
||||
// the same leaf cluster of type EDS and a different leaf cluster of type
|
||||
// LogicalDNS and verifies that the load balancing configuration pushed to the
|
||||
// priority LB policy contains the expected config.
|
||||
func (s) TestAggregateClusterSuccess_ThenUpdateChildClusters(t *testing.T) {
|
||||
dnsTargetCh, dnsR := setupDNS(t)
|
||||
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
|
||||
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
|
||||
|
||||
// Configure the management server with the aggregate cluster resource
|
||||
// pointing to two child clusters, one EDS and one LogicalDNS. Include the
|
||||
// resource corresponding to the EDS cluster here, but don't include
|
||||
// resource corresponding to the LogicalDNS cluster yet.
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}),
|
||||
e2e.DefaultCluster(edsClusterName, serviceName, e2e.SecurityLevelNone),
|
||||
},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that no configuration is pushed to the child policy yet, because
|
||||
// not all clusters making up the aggregate cluster have been resolved yet.
|
||||
select {
|
||||
case cfg := <-lbCfgCh:
|
||||
t.Fatalf("Child policy received configuration when not expected to: %s", pretty.ToJSON(cfg))
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
}
|
||||
|
||||
// Now configure the LogicalDNS cluster in the management server. This
|
||||
// should result in configuration being pushed down to the child policy.
|
||||
resources.Clusters = append(resources.Clusters, makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort))
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyDNSResolution(ctx, t, dnsTargetCh, dnsR, dnsHostName, dnsPort)
|
||||
|
||||
wantChildCfg := &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(edsClusterName),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
"priority-1": {Config: createPriorityConfig(dnsClusterName)},
|
||||
},
|
||||
Priorities: []string{"priority-0-0", "priority-1"},
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
const dnsClusterNameNew = dnsClusterName + "-new"
|
||||
const dnsHostNameNew = dnsHostName + "-new"
|
||||
resources = e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterNameNew}),
|
||||
e2e.DefaultCluster(edsClusterName, serviceName, e2e.SecurityLevelNone),
|
||||
makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort),
|
||||
makeLogicalDNSClusterResource(dnsClusterNameNew, dnsHostNameNew, dnsPort),
|
||||
},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyDNSResolution(ctx, t, dnsTargetCh, dnsR, dnsHostNameNew, dnsPort)
|
||||
|
||||
wantChildCfg = &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(edsClusterName),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
"priority-2": {Config: createPriorityConfig(dnsClusterNameNew)},
|
||||
},
|
||||
Priorities: []string{"priority-0-0", "priority-2"},
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the case where the cluster resource requested is an aggregate cluster
|
||||
// root pointing to two child clusters, one of type EDS and the other of type
|
||||
// LogicalDNS. The test verifies that the load balancing configuration pushed to
|
||||
// the priority LB policy contains the discovery mechanisms for both child
|
||||
// clusters. The test then updates the root cluster resource requested by the
|
||||
// cds LB policy to a leaf cluster of type EDS and verifies the load balancing
|
||||
// configuration pushed to the priority LB policy contains a single discovery
|
||||
// mechanism.
|
||||
func (s) TestAggregateClusterSuccess_ThenChangeRootToEDS(t *testing.T) {
|
||||
dnsTargetCh, dnsR := setupDNS(t)
|
||||
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
|
||||
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
|
||||
|
||||
// Configure the management server with the aggregate cluster resource
|
||||
// pointing to two child clusters.
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}),
|
||||
e2e.DefaultCluster(edsClusterName, serviceName, e2e.SecurityLevelNone),
|
||||
makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort),
|
||||
},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyDNSResolution(ctx, t, dnsTargetCh, dnsR, dnsHostName, dnsPort)
|
||||
|
||||
wantChildCfg := &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(edsClusterName),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
"priority-1": {Config: createPriorityConfig(dnsClusterName)},
|
||||
},
|
||||
Priorities: []string{"priority-0-0", "priority-1"},
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resources = e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone),
|
||||
makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort),
|
||||
},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Since the service name of the EDS cluster remains same, same priority name
|
||||
// is used.
|
||||
wantChildCfg = &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(clusterName),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
},
|
||||
Priorities: []string{"priority-0-0"},
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the case where a requested cluster resource switches between being a
|
||||
// leaf and an aggregate cluster pointing to an EDS and LogicalDNS child
|
||||
// cluster. In each of these cases, the test verifies that the load balancing
|
||||
// configuration pushed to the priority LB policy contains the expected config.
|
||||
func (s) TestAggregatedClusterSuccess_SwitchBetweenLeafAndAggregate(t *testing.T) {
|
||||
dnsTargetCh, dnsR := setupDNS(t)
|
||||
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
|
||||
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
|
||||
|
||||
// Start off with the requested cluster being a leaf EDS cluster.
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone)},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantChildCfg := &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(clusterName),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
},
|
||||
Priorities: []string{"priority-0-0"},
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Switch the requested cluster to be an aggregate cluster pointing to two
|
||||
// child clusters.
|
||||
resources = e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterName, []string{edsClusterName, dnsClusterName}),
|
||||
e2e.DefaultCluster(edsClusterName, serviceName, e2e.SecurityLevelNone),
|
||||
makeLogicalDNSClusterResource(dnsClusterName, dnsHostName, dnsPort),
|
||||
},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
verifyDNSResolution(ctx, t, dnsTargetCh, dnsR, dnsHostName, dnsPort)
|
||||
|
||||
wantChildCfg = &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(edsClusterName),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
"priority-1": {Config: createPriorityConfig(dnsClusterName)},
|
||||
},
|
||||
Priorities: []string{"priority-0-0", "priority-1"},
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Switch the cluster back to a leaf EDS cluster.
|
||||
resources = e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, serviceName, e2e.SecurityLevelNone)},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantChildCfg = &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(clusterName),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
},
|
||||
Priorities: []string{"priority-0-0"},
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where an aggregate cluster exceeds the maximum depth,
|
||||
// which is 16. Verifies that the channel moves to TRANSIENT_FAILURE, and the
|
||||
// error is propagated to RPC callers. The test then modifies the graph to no
|
||||
// longer exceed maximum depth, but be at the maximum allowed depth, and
|
||||
// verifies that an RPC can be made successfully.
|
||||
func (s) TestAggregatedClusterFailure_ExceedsMaxStackDepth(t *testing.T) {
|
||||
mgmtServer, nodeID, cc := setupWithManagementServer(t, nil, nil)
|
||||
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterName, []string{clusterName + "-1"}),
|
||||
makeAggregateClusterResource(clusterName+"-1", []string{clusterName + "-2"}),
|
||||
makeAggregateClusterResource(clusterName+"-2", []string{clusterName + "-3"}),
|
||||
makeAggregateClusterResource(clusterName+"-3", []string{clusterName + "-4"}),
|
||||
makeAggregateClusterResource(clusterName+"-4", []string{clusterName + "-5"}),
|
||||
makeAggregateClusterResource(clusterName+"-5", []string{clusterName + "-6"}),
|
||||
makeAggregateClusterResource(clusterName+"-6", []string{clusterName + "-7"}),
|
||||
makeAggregateClusterResource(clusterName+"-7", []string{clusterName + "-8"}),
|
||||
makeAggregateClusterResource(clusterName+"-8", []string{clusterName + "-9"}),
|
||||
makeAggregateClusterResource(clusterName+"-9", []string{clusterName + "-10"}),
|
||||
makeAggregateClusterResource(clusterName+"-10", []string{clusterName + "-11"}),
|
||||
makeAggregateClusterResource(clusterName+"-11", []string{clusterName + "-12"}),
|
||||
makeAggregateClusterResource(clusterName+"-12", []string{clusterName + "-13"}),
|
||||
makeAggregateClusterResource(clusterName+"-13", []string{clusterName + "-14"}),
|
||||
makeAggregateClusterResource(clusterName+"-14", []string{clusterName + "-15"}),
|
||||
makeAggregateClusterResource(clusterName+"-15", []string{clusterName + "-16"}),
|
||||
e2e.DefaultCluster(clusterName+"-16", serviceName, e2e.SecurityLevelNone),
|
||||
},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
|
||||
|
||||
const wantErr = "aggregate cluster graph exceeds max depth"
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
_, err := client.EmptyCall(ctx, &testpb.Empty{})
|
||||
if code := status.Code(err); code != codes.Unavailable {
|
||||
t.Fatalf("EmptyCall() failed with code: %v, want %v", code, codes.Unavailable)
|
||||
}
|
||||
if err != nil && !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("EmptyCall() failed with err: %v, want err containing: %v", err, wantErr)
|
||||
}
|
||||
|
||||
// Start a test service backend.
|
||||
server := stubserver.StartTestService(t, nil)
|
||||
t.Cleanup(server.Stop)
|
||||
|
||||
// Update the aggregate cluster resource to no longer exceed max depth, and
|
||||
// be at the maximum depth allowed.
|
||||
resources = e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterName, []string{clusterName + "-1"}),
|
||||
makeAggregateClusterResource(clusterName+"-1", []string{clusterName + "-2"}),
|
||||
makeAggregateClusterResource(clusterName+"-2", []string{clusterName + "-3"}),
|
||||
makeAggregateClusterResource(clusterName+"-3", []string{clusterName + "-4"}),
|
||||
makeAggregateClusterResource(clusterName+"-4", []string{clusterName + "-5"}),
|
||||
makeAggregateClusterResource(clusterName+"-5", []string{clusterName + "-6"}),
|
||||
makeAggregateClusterResource(clusterName+"-6", []string{clusterName + "-7"}),
|
||||
makeAggregateClusterResource(clusterName+"-7", []string{clusterName + "-8"}),
|
||||
makeAggregateClusterResource(clusterName+"-8", []string{clusterName + "-9"}),
|
||||
makeAggregateClusterResource(clusterName+"-9", []string{clusterName + "-10"}),
|
||||
makeAggregateClusterResource(clusterName+"-10", []string{clusterName + "-11"}),
|
||||
makeAggregateClusterResource(clusterName+"-11", []string{clusterName + "-12"}),
|
||||
makeAggregateClusterResource(clusterName+"-12", []string{clusterName + "-13"}),
|
||||
makeAggregateClusterResource(clusterName+"-13", []string{clusterName + "-14"}),
|
||||
makeAggregateClusterResource(clusterName+"-14", []string{clusterName + "-15"}),
|
||||
e2e.DefaultCluster(clusterName+"-15", serviceName, e2e.SecurityLevelNone),
|
||||
},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{testutils.ParsePort(t, server.Address)})},
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that a successful RPC can be made.
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests a diamond shaped aggregate cluster (A->[B,C]; B->D; C->D). Verifies
|
||||
// that the load balancing configuration pushed to the priority LB
|
||||
// policy specifies cluster D only once. Also verifies that configuration is
|
||||
// pushed only after all child clusters are resolved.
|
||||
func (s) TestAggregatedClusterSuccess_DiamondDependency(t *testing.T) {
|
||||
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
|
||||
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
|
||||
|
||||
// Configure the management server with an aggregate cluster resource having
|
||||
// a diamond dependency pattern, (A->[B,C]; B->D; C->D). Includes resources
|
||||
// for cluster A, B and D, but don't include the resource for cluster C yet.
|
||||
// This will help us verify that no configuration is pushed to the child
|
||||
// policy until the whole cluster graph is resolved.
|
||||
const (
|
||||
clusterNameA = clusterName // cluster name in cds LB policy config
|
||||
clusterNameB = clusterName + "-B"
|
||||
clusterNameC = clusterName + "-C"
|
||||
clusterNameD = clusterName + "-D"
|
||||
)
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterNameA, []string{clusterNameB, clusterNameC}),
|
||||
makeAggregateClusterResource(clusterNameB, []string{clusterNameD}),
|
||||
e2e.DefaultCluster(clusterNameD, serviceName, e2e.SecurityLevelNone),
|
||||
},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that no configuration is pushed to the child policy yet, because
|
||||
// not all clusters making up the aggregate cluster have been resolved yet.
|
||||
select {
|
||||
case cfg := <-lbCfgCh:
|
||||
t.Fatalf("Child policy received configuration when not expected to: %s", pretty.ToJSON(cfg))
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
}
|
||||
|
||||
// Now configure the resource for cluster C in the management server,
|
||||
// thereby completing the cluster graph. This should result in configuration
|
||||
// being pushed down to the child policy.
|
||||
resources.Clusters = append(resources.Clusters, makeAggregateClusterResource(clusterNameC, []string{clusterNameD}))
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantChildCfg := &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(clusterNameD),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
},
|
||||
Priorities: []string{"priority-0-0"},
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the case where the aggregate cluster graph contains duplicates (A->[B,
|
||||
// C]; B->[C, D]). Verifies that the load balancing configuration pushed to the
|
||||
// priority LB policy does not contain duplicates, and that the priority
|
||||
// corresponding to cluster C is higher than that for cluster D. Also verifies
|
||||
// that the configuration is pushed only after all child clusters are resolved.
|
||||
func (s) TestAggregatedClusterSuccess_IgnoreDups(t *testing.T) {
|
||||
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
|
||||
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, nil)
|
||||
|
||||
// Configure the management server with an aggregate cluster resource that
|
||||
// has duplicates in the graph, (A->[B, C]; B->[C, D]). Include resources
|
||||
// for clusters A, B and D, but don't configure the resource for cluster C
|
||||
// yet. This will help us verify that no configuration is pushed to the
|
||||
// child policy until the whole cluster graph is resolved.
|
||||
const (
|
||||
clusterNameA = clusterName // cluster name in cds LB policy config
|
||||
clusterNameB = clusterName + "-B"
|
||||
clusterNameC = clusterName + "-C"
|
||||
clusterNameD = clusterName + "-D"
|
||||
)
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterNameA, []string{clusterNameB, clusterNameC}),
|
||||
makeAggregateClusterResource(clusterNameB, []string{clusterNameC, clusterNameD}),
|
||||
e2e.DefaultCluster(clusterNameD, serviceName, e2e.SecurityLevelNone),
|
||||
},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{port})},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that no configuration is pushed to the child policy yet, because
|
||||
// not all clusters making up the aggregate cluster have been resolved yet.
|
||||
select {
|
||||
case cfg := <-lbCfgCh:
|
||||
t.Fatalf("Child policy received configuration when not expected to: %s", pretty.ToJSON(cfg))
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
}
|
||||
|
||||
// Now configure the resource for cluster C in the management server,
|
||||
// thereby completing the cluster graph. This should result in configuration
|
||||
// being pushed down to the child policy.
|
||||
resources.Clusters = append(resources.Clusters, e2e.DefaultCluster(clusterNameC, edsClusterName, e2e.SecurityLevelNone))
|
||||
resources.Endpoints = append(resources.Endpoints, e2e.DefaultEndpoint(edsClusterName, "localhost", []uint32{1234}))
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantChildCfg := &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(clusterNameC),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
"priority-1-0": {
|
||||
Config: createPriorityConfig(clusterNameD),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
},
|
||||
Priorities: []string{"priority-0-0", "priority-1-0"},
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where the aggregate cluster graph has a node that has
|
||||
// child node of itself. The case for this is A -> A, and since there is no base
|
||||
// cluster (EDS or Logical DNS), no configuration should be pushed to the child
|
||||
// policy. The channel is expected to move to TRANSIENT_FAILURE and RPCs are
|
||||
// expected to fail with code UNAVAILABLE and an error message specifying that
|
||||
// the aggregate cluster graph has no leaf clusters. Then the test updates A -> B,
|
||||
// where B is a leaf EDS cluster. Verifies that configuration is pushed to the
|
||||
// child policy and that an RPC can be successfully made.
|
||||
func (s) TestAggregatedCluster_NodeChildOfItself(t *testing.T) {
|
||||
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
|
||||
mgmtServer, nodeID, cc := setupWithManagementServer(t, nil, nil)
|
||||
|
||||
const (
|
||||
clusterNameA = clusterName // cluster name in cds LB policy config
|
||||
clusterNameB = clusterName + "-B"
|
||||
)
|
||||
// Configure the management server with an aggregate cluster resource whose
|
||||
// child is itself.
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{makeAggregateClusterResource(clusterNameA, []string{clusterNameA})},
|
||||
SkipValidation: true,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case cfg := <-lbCfgCh:
|
||||
t.Fatalf("Child policy received configuration when not expected to: %s", pretty.ToJSON(cfg))
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
}
|
||||
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
|
||||
|
||||
// Verify that the RPC fails with expected code.
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
_, err := client.EmptyCall(ctx, &testpb.Empty{})
|
||||
if gotCode, wantCode := status.Code(err), codes.Unavailable; gotCode != wantCode {
|
||||
t.Fatalf("EmptyCall() failed with code: %v, want %v", gotCode, wantCode)
|
||||
}
|
||||
const wantErr = "aggregate cluster graph has no leaf clusters"
|
||||
if !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("EmptyCall() failed with err: %v, want error containing %s", err, wantErr)
|
||||
}
|
||||
|
||||
// Start a test service backend.
|
||||
server := stubserver.StartTestService(t, nil)
|
||||
t.Cleanup(server.Stop)
|
||||
|
||||
// Update the aggregate cluster to point to a leaf EDS cluster.
|
||||
resources = e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterNameA, []string{clusterNameB}),
|
||||
e2e.DefaultCluster(clusterNameB, serviceName, e2e.SecurityLevelNone),
|
||||
},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{testutils.ParsePort(t, server.Address)})},
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify the configuration pushed to the child policy.
|
||||
wantChildCfg := &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(clusterNameB),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
},
|
||||
Priorities: []string{"priority-0-0"},
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that a successful RPC can be made.
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where the aggregate cluster graph contains a cycle and
|
||||
// contains no leaf clusters. The case used here is [A -> B, B -> A]. As there
|
||||
// are no leaf clusters in this graph, no configuration should be pushed to the
|
||||
// child policy. The channel is expected to move to TRANSIENT_FAILURE and RPCs
|
||||
// are expected to fail with code UNAVAILABLE and an error message specifying
|
||||
// that the aggregate cluster graph has no leaf clusters.
|
||||
func (s) TestAggregatedCluster_CycleWithNoLeafNode(t *testing.T) {
|
||||
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
|
||||
mgmtServer, nodeID, cc := setupWithManagementServer(t, nil, nil)
|
||||
|
||||
const (
|
||||
clusterNameA = clusterName // cluster name in cds LB policy config
|
||||
clusterNameB = clusterName + "-B"
|
||||
)
|
||||
// Configure the management server with an aggregate cluster resource graph
|
||||
// that contains a cycle and no leaf clusters.
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterNameA, []string{clusterNameB}),
|
||||
makeAggregateClusterResource(clusterNameB, []string{clusterNameA}),
|
||||
},
|
||||
SkipValidation: true,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
select {
|
||||
case cfg := <-lbCfgCh:
|
||||
t.Fatalf("Child policy received configuration when not expected to: %s", pretty.ToJSON(cfg))
|
||||
case <-time.After(defaultTestShortTimeout):
|
||||
}
|
||||
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
|
||||
|
||||
// Verify that the RPC fails with expected code.
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
_, err := client.EmptyCall(ctx, &testpb.Empty{})
|
||||
if gotCode, wantCode := status.Code(err), codes.Unavailable; gotCode != wantCode {
|
||||
t.Fatalf("EmptyCall() failed with code: %v, want %v", gotCode, wantCode)
|
||||
}
|
||||
const wantErr = "aggregate cluster graph has no leaf clusters"
|
||||
if !strings.Contains(err.Error(), wantErr) {
|
||||
t.Fatalf("EmptyCall() failed with err: %v, want %s", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where the aggregate cluster graph contains a cycle and
|
||||
// also contains a leaf cluster. The case used here is [A -> B, B -> A, C]. As
|
||||
// there is a leaf cluster in this graph , configuration should be pushed to the
|
||||
// child policy and RPCs should get routed to that leaf cluster.
|
||||
func (s) TestAggregatedCluster_CycleWithLeafNode(t *testing.T) {
|
||||
lbCfgCh, _, _, _ := registerWrappedPriorityPolicy(t)
|
||||
mgmtServer, nodeID, cc := setupWithManagementServer(t, nil, nil)
|
||||
|
||||
// Start a test service backend.
|
||||
server := stubserver.StartTestService(t, nil)
|
||||
t.Cleanup(server.Stop)
|
||||
|
||||
const (
|
||||
clusterNameA = clusterName // cluster name in cds LB policy config
|
||||
clusterNameB = clusterName + "-B"
|
||||
clusterNameC = clusterName + "-C"
|
||||
)
|
||||
// Configure the management server with an aggregate cluster resource graph
|
||||
// that contains a cycle, but also contains a leaf cluster.
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterNameA, []string{clusterNameB}),
|
||||
makeAggregateClusterResource(clusterNameB, []string{clusterNameA, clusterNameC}),
|
||||
e2e.DefaultCluster(clusterNameC, serviceName, e2e.SecurityLevelNone),
|
||||
},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(serviceName, host, []uint32{testutils.ParsePort(t, server.Address)})},
|
||||
SkipValidation: true,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := mgmtServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify the configuration pushed to the child policy.
|
||||
wantChildCfg := &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: createPriorityConfig(clusterNameC),
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
},
|
||||
Priorities: []string{"priority-0-0"},
|
||||
}
|
||||
if err := compareLoadBalancingConfig(ctx, lbCfgCh, wantChildCfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Verify that a successful RPC can be made.
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}, grpc.WaitForReady(true)); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Tests the scenario where the cluster tree changes, and verifies that the
|
||||
// watchers for the cds balancer are updated accordingly. That is the cluster
|
||||
// removed from the tree no longer has a watcher and the new cluster added has a
|
||||
// new watcher.
|
||||
func (s) TestWatchers(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
cdsResourceRequestedCh := make(chan []string, 1)
|
||||
onStreamReq := func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
|
||||
if req.GetTypeUrl() == version.V3ClusterURL {
|
||||
if len(req.GetResourceNames()) > 0 {
|
||||
select {
|
||||
case cdsResourceRequestedCh <- req.GetResourceNames():
|
||||
case <-ctx.Done():
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
mgmtServer, nodeID, _ := setupWithManagementServer(t, nil, onStreamReq)
|
||||
|
||||
const (
|
||||
clusterA = clusterName
|
||||
clusterB = clusterName + "-B"
|
||||
clusterC = clusterName + "-C"
|
||||
clusterD = clusterName + "-D"
|
||||
)
|
||||
|
||||
// Initial CDS resources: A -> B, C
|
||||
initialResources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(target, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, target, clusterA)},
|
||||
Clusters: []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterA, []string{clusterB, clusterC}),
|
||||
},
|
||||
SkipValidation: true,
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, initialResources); err != nil {
|
||||
t.Fatalf("Update failed: %v", err)
|
||||
}
|
||||
wantNames := []string{clusterA, clusterB, clusterC}
|
||||
if err := waitForResourceNames(ctx, cdsResourceRequestedCh, wantNames); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Update the CDS resources to remove cluster C and add cluster D.
|
||||
initialResources.Clusters = []*v3clusterpb.Cluster{
|
||||
makeAggregateClusterResource(clusterA, []string{clusterB, clusterD}),
|
||||
}
|
||||
if err := mgmtServer.Update(ctx, initialResources); err != nil {
|
||||
t.Fatalf("Update failed: %v", err)
|
||||
}
|
||||
wantNames = []string{clusterA, clusterB, clusterD}
|
||||
if err := waitForResourceNames(ctx, cdsResourceRequestedCh, wantNames); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
472
internal/xds/balancer/cdsbalancer/cdsbalancer.go
Normal file
472
internal/xds/balancer/cdsbalancer/cdsbalancer.go
Normal file
@@ -0,0 +1,472 @@
|
||||
/*
|
||||
* 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 cdsbalancer implements a balancer to handle CDS responses.
|
||||
package cdsbalancer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/attributes"
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/balancer/base"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/internal/balancer/nop"
|
||||
"google.golang.org/grpc/internal/grpclog"
|
||||
"google.golang.org/grpc/internal/pretty"
|
||||
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||||
"google.golang.org/grpc/internal/xds/balancer/outlierdetection"
|
||||
"google.golang.org/grpc/internal/xds/balancer/priority"
|
||||
"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"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
)
|
||||
|
||||
const cdsName = "cds_experimental"
|
||||
|
||||
var (
|
||||
// newChildBalancer is a helper function to build a new priority balancer
|
||||
// and will be overridden in unittests.
|
||||
newChildBalancer = func(cc balancer.ClientConn, opts balancer.BuildOptions) (balancer.Balancer, error) {
|
||||
builder := balancer.Get(priority.Name)
|
||||
if builder == nil {
|
||||
return nil, fmt.Errorf("xds: no balancer builder with name %v", priority.Name)
|
||||
}
|
||||
// We directly pass the parent clientConn to the underlying priority
|
||||
// balancer because the cdsBalancer does not deal with subConns.
|
||||
return builder.Build(cc, opts), nil
|
||||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
balancer.Register(bb{})
|
||||
}
|
||||
|
||||
// bb implements the balancer.Builder interface to help build a cdsBalancer.
|
||||
// It also implements the balancer.ConfigParser interface to help parse the
|
||||
// JSON service config, to be passed to the cdsBalancer.
|
||||
type bb struct{}
|
||||
|
||||
// Build creates a new CDS balancer with the ClientConn.
|
||||
func (bb) Build(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer {
|
||||
builder := balancer.Get(priority.Name)
|
||||
if builder == nil {
|
||||
// Shouldn't happen, registered through imported Priority builder. Still,
|
||||
// defensive programming.
|
||||
logger.Errorf("%q LB policy is needed but not registered", priority.Name)
|
||||
return nop.NewBalancer(cc, fmt.Errorf("%q LB policy is needed but not registered", priority.Name))
|
||||
}
|
||||
parser, ok := builder.(balancer.ConfigParser)
|
||||
if !ok {
|
||||
// Shouldn't happen, imported Priority builder has this method.
|
||||
logger.Errorf("%q LB policy does not implement a config parser", priority.Name)
|
||||
return nop.NewBalancer(cc, fmt.Errorf("%q LB policy does not implement a config parser", priority.Name))
|
||||
}
|
||||
|
||||
b := &cdsBalancer{
|
||||
bOpts: opts,
|
||||
childConfigParser: parser,
|
||||
clusterConfigs: make(map[string]*xdsresource.ClusterResult),
|
||||
priorityConfigs: make(map[string]*priorityConfig),
|
||||
cc: cc,
|
||||
}
|
||||
b.logger = prefixLogger(b)
|
||||
b.logger.Infof("Created")
|
||||
return b
|
||||
}
|
||||
|
||||
// Name returns the name of balancers built by this builder.
|
||||
func (bb) Name() string {
|
||||
return cdsName
|
||||
}
|
||||
|
||||
// lbConfig represents the loadBalancingConfig section of the service config
|
||||
// for the cdsBalancer.
|
||||
type lbConfig struct {
|
||||
serviceconfig.LoadBalancingConfig
|
||||
ClusterName string `json:"cluster"`
|
||||
IsDynamic bool `json:"isDynamic"`
|
||||
}
|
||||
|
||||
// ParseConfig parses the JSON load balancer config provided into an
|
||||
// internal form or returns an error if the config is invalid.
|
||||
func (bb) ParseConfig(c json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
var cfg lbConfig
|
||||
if err := json.Unmarshal(c, &cfg); err != nil {
|
||||
return nil, fmt.Errorf("xds: unable to unmarshal lbconfig: %s, error: %v", string(c), err)
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
// cdsBalancer implements a CDS based LB policy. It instantiates a
|
||||
// cluster_resolver balancer to further resolve the serviceName received from
|
||||
// CDS, into localities and endpoints. Implements the balancer.Balancer
|
||||
// interface which is exposed to gRPC and implements the balancer.ClientConn
|
||||
// interface which is exposed to the cluster_resolver balancer.
|
||||
type cdsBalancer struct {
|
||||
// The following fields are initialized at build time and are either
|
||||
// read-only after that or provide their own synchronization, and therefore
|
||||
// do not need to be guarded by a mutex.
|
||||
cc balancer.ClientConn // ClientConn interface passed to child LB.
|
||||
bOpts balancer.BuildOptions // BuildOptions passed to child LB.
|
||||
childConfigParser balancer.ConfigParser // Config parser for cluster_resolver LB policy.
|
||||
logger *grpclog.PrefixLogger // Prefix logger for all logging.
|
||||
|
||||
// All fields below are accessed only from methods implementing the
|
||||
// balancer.Balancer interface. Since gRPC guarantees that these methods are
|
||||
// never invoked concurrently, no additional synchronization is required to
|
||||
// protect access to these fields.
|
||||
xdsClient xdsclient.XDSClient
|
||||
childLB balancer.Balancer // Child policy, built upon resolution of the cluster graph.
|
||||
clusterConfigs map[string]*xdsresource.ClusterResult // Cluster name to the last received result for that cluster.
|
||||
priorityConfigs map[string]*priorityConfig // Hostname to priority config for that leaf cluster.
|
||||
lbCfg *lbConfig // Current load balancing configuration.
|
||||
priorities []*priorityConfig // List of priorities in the order.
|
||||
unsubscribe func() // For dynamic cluster unsubscription.
|
||||
isSubscribed bool // True if a dynamic cluster has been subscribed to.
|
||||
clusterSubscriber xdsdepmgr.ClusterSubscriber // To subscribe to dynamic cluster resource.
|
||||
xdsLBPolicy internalserviceconfig.BalancerConfig // Stores the locality and endpoint picking policy.
|
||||
attributes *attributes.Attributes // Attributes from resolver state.
|
||||
serviceConfig *serviceconfig.ParseResult
|
||||
// Each new leaf cluster needs a child name generator to reuse child policy
|
||||
// names. But to make sure the names across leaf clusters doesn't conflict,
|
||||
// we need a seq ID. This ID is incremented for each new cluster.
|
||||
childNameGeneratorSeqID uint64
|
||||
}
|
||||
|
||||
// UpdateClientConnState receives the serviceConfig, xdsConfig,
|
||||
// ClusterSubscriber and the xdsClient object from the xdsResolver. If an error
|
||||
// is encountered, the parent (clustermanager) sets the corresponding cluster’s
|
||||
// picker to transient_failure. Otherwise, the received configuration is
|
||||
// processed and forwarded to the appropriate child policy.
|
||||
func (b *cdsBalancer) UpdateClientConnState(state balancer.ClientConnState) error {
|
||||
if b.xdsClient == nil {
|
||||
c := xdsclient.FromResolverState(state.ResolverState)
|
||||
if c == nil {
|
||||
b.logger.Warningf("Received balancer config with no xDS client")
|
||||
return balancer.ErrBadResolverState
|
||||
}
|
||||
b.xdsClient = c
|
||||
}
|
||||
b.logger.Infof("Received balancer config update: %s", pretty.ToJSON(state.BalancerConfig))
|
||||
|
||||
xdsConfig := xdsresource.XDSConfigFromResolverState(state.ResolverState)
|
||||
if xdsConfig == nil {
|
||||
b.logger.Warningf("Received balancer config with no xDS config")
|
||||
return balancer.ErrBadResolverState
|
||||
}
|
||||
b.clusterConfigs = xdsConfig.Clusters
|
||||
b.clusterSubscriber = xdsdepmgr.XDSClusterSubscriberFromResolverState(state.ResolverState)
|
||||
if b.clusterSubscriber == nil {
|
||||
b.logger.Warningf("Received balancer config with no cluster subscriber")
|
||||
return balancer.ErrBadResolverState
|
||||
}
|
||||
// The errors checked here should ideally never happen because the
|
||||
// ServiceConfig in this case is prepared by the xdsResolver and is not
|
||||
// something that is received on the wire.
|
||||
lbCfg, ok := state.BalancerConfig.(*lbConfig)
|
||||
if !ok {
|
||||
b.logger.Warningf("Received unexpected balancer config type: %T", state.BalancerConfig)
|
||||
return balancer.ErrBadResolverState
|
||||
}
|
||||
if lbCfg.ClusterName == "" {
|
||||
b.logger.Warningf("Received balancer config with no cluster name")
|
||||
return balancer.ErrBadResolverState
|
||||
}
|
||||
|
||||
b.lbCfg = lbCfg
|
||||
b.serviceConfig = state.ResolverState.ServiceConfig
|
||||
b.attributes = state.ResolverState.Attributes
|
||||
return b.handleXDSConfigUpdate()
|
||||
}
|
||||
|
||||
// handleXDSConfigUpdate processes the XDSConfig update from the xDS resolver.
|
||||
func (b *cdsBalancer) handleXDSConfigUpdate() error {
|
||||
clusterName := b.lbCfg.ClusterName
|
||||
|
||||
// If the cluster is dynamic and we dont have a subscription yet, create
|
||||
// one.
|
||||
if b.lbCfg.IsDynamic && !b.isSubscribed {
|
||||
b.unsubscribe = b.clusterSubscriber.SubscribeToCluster(clusterName)
|
||||
b.isSubscribed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
clusterUpdate, ok := b.clusterConfigs[clusterName]
|
||||
if !ok {
|
||||
// If the cluster is missing from the config, check if it is dynamic.
|
||||
// For dynamic clusters, the xDS config may be updated before the
|
||||
// corresponding cluster resource is received. This should never occur
|
||||
// for static clusters.
|
||||
if b.lbCfg.IsDynamic {
|
||||
return nil
|
||||
}
|
||||
return b.annotateErrorWithNodeID(fmt.Errorf("did not find the cluster %q in XDSConfig", clusterName))
|
||||
}
|
||||
// If the cluster resource has an error, return the error.
|
||||
if clusterUpdate.Err != nil {
|
||||
return clusterUpdate.Err
|
||||
}
|
||||
return b.handleClusterUpdate()
|
||||
}
|
||||
|
||||
// handleClusterUpdate handles a good XDSConfig update from the xDS resolver.
|
||||
// Builds the child policy config and pushes it down.
|
||||
func (b *cdsBalancer) handleClusterUpdate() error {
|
||||
clusterName := b.lbCfg.ClusterName
|
||||
clusterConfig := b.clusterConfigs[clusterName].Config
|
||||
|
||||
var newPriorities []*priorityConfig
|
||||
switch clusterConfig.Cluster.ClusterType {
|
||||
case xdsresource.ClusterTypeEDS, xdsresource.ClusterTypeLogicalDNS:
|
||||
p := b.updatePriorityConfig(clusterName, &clusterConfig)
|
||||
newPriorities = append(newPriorities, p)
|
||||
case xdsresource.ClusterTypeAggregate:
|
||||
for _, leaf := range clusterConfig.AggregateConfig.LeafClusters {
|
||||
leafCluster := b.clusterConfigs[leaf]
|
||||
// Update priority config for leaf clusters.
|
||||
p := b.updatePriorityConfig(leaf, &leafCluster.Config)
|
||||
newPriorities = append(newPriorities, p)
|
||||
}
|
||||
}
|
||||
b.priorities = newPriorities
|
||||
|
||||
if err := b.updateOutlierDetection(); err != nil {
|
||||
return b.annotateErrorWithNodeID(fmt.Errorf("failed to correctly update Outlier Detection config %v", err))
|
||||
}
|
||||
|
||||
// The LB policy is configured by the root cluster.
|
||||
if err := json.Unmarshal(clusterConfig.Cluster.LBPolicy, &b.xdsLBPolicy); err != nil {
|
||||
return b.annotateErrorWithNodeID(fmt.Errorf("error unmarshalling xDS LB Policy: %v", err))
|
||||
}
|
||||
if err := b.updateChildConfig(); err != nil {
|
||||
return b.annotateErrorWithNodeID(err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateChildConfig builds child policy configuration using endpoint addresses
|
||||
// returned from the XDSConfig and child policy configuration.
|
||||
//
|
||||
// A child policy is created if one doesn't already exist. The newly built
|
||||
// configuration is then pushed to the child policy.
|
||||
func (b *cdsBalancer) updateChildConfig() error {
|
||||
if b.childLB == nil {
|
||||
childLB, err := newChildBalancer(b.cc, b.bOpts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create child policy of type %s: %v", priority.Name, err)
|
||||
}
|
||||
b.childLB = childLB
|
||||
}
|
||||
|
||||
childCfgBytes, endpoints, err := buildPriorityConfigJSON(b.priorities, &b.xdsLBPolicy)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build child policy config: %v", err)
|
||||
}
|
||||
childCfg, err := b.childConfigParser.ParseConfig(childCfgBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse child policy config. This should never happen because the config was generated: %v", err)
|
||||
}
|
||||
if b.logger.V(2) {
|
||||
b.logger.Infof("Built child policy config: %s", pretty.ToJSON(childCfg))
|
||||
}
|
||||
|
||||
for i := range endpoints {
|
||||
for j := range endpoints[i].Addresses {
|
||||
addr := endpoints[i].Addresses[j]
|
||||
addr.BalancerAttributes = endpoints[i].Attributes
|
||||
// BalancerAttributes are used for the following:
|
||||
// * Authority Override.
|
||||
// * grpc.lb.backend_service metric label propagation.
|
||||
// See https://github.com/grpc/grpc-go/issues/6472
|
||||
endpoints[i].Addresses[j] = addr
|
||||
}
|
||||
}
|
||||
if err := b.childLB.UpdateClientConnState(balancer.ClientConnState{
|
||||
ResolverState: resolver.State{
|
||||
Endpoints: endpoints,
|
||||
ServiceConfig: b.serviceConfig,
|
||||
Attributes: b.attributes,
|
||||
},
|
||||
BalancerConfig: childCfg,
|
||||
}); err != nil {
|
||||
return fmt.Errorf("failed to push config to child policy: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// updatePriorityConfig updates the priority configuration for the specified EDS
|
||||
// or DNS cluster, creating it if it does not already exist.
|
||||
func (b *cdsBalancer) updatePriorityConfig(clusterName string, clusterConfig *xdsresource.ClusterConfig) *priorityConfig {
|
||||
name := hostName(clusterName, *clusterConfig.Cluster)
|
||||
pc, ok := b.priorityConfigs[name]
|
||||
if !ok {
|
||||
pc = &priorityConfig{
|
||||
childNameGen: newNameGenerator(b.childNameGeneratorSeqID),
|
||||
}
|
||||
b.priorityConfigs[name] = pc
|
||||
// Increment the seq ID for the next new cluster. This is done to make
|
||||
// sure that the child policy names generated for different clusters
|
||||
// don't conflict with each other.
|
||||
b.childNameGeneratorSeqID++
|
||||
}
|
||||
pc.clusterConfig = clusterConfig
|
||||
return pc
|
||||
}
|
||||
|
||||
// updateOutlierDetection updates Outlier Detection config for all priorities.
|
||||
func (b *cdsBalancer) updateOutlierDetection() error {
|
||||
odBuilder := balancer.Get(outlierdetection.Name)
|
||||
if odBuilder == nil {
|
||||
// Shouldn't happen, registered through imported Outlier Detection,
|
||||
// defensive programming.
|
||||
return fmt.Errorf("%q LB policy is needed but not registered", outlierdetection.Name)
|
||||
}
|
||||
|
||||
odParser, ok := odBuilder.(balancer.ConfigParser)
|
||||
if !ok {
|
||||
// Shouldn't happen, imported Outlier Detection builder has this method.
|
||||
return fmt.Errorf("%q LB policy does not implement a config parser", outlierdetection.Name)
|
||||
}
|
||||
|
||||
for _, p := range b.priorities {
|
||||
// Update Outlier Detection Config.
|
||||
odJSON := p.clusterConfig.Cluster.OutlierDetection
|
||||
if odJSON == nil {
|
||||
odJSON = json.RawMessage(`{}`)
|
||||
}
|
||||
|
||||
lbCfg, err := odParser.ParseConfig(odJSON)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing Outlier Detection config %v: %v", odJSON, err)
|
||||
}
|
||||
|
||||
odCfg, ok := lbCfg.(*outlierdetection.LBConfig)
|
||||
if !ok {
|
||||
// Shouldn't happen, Parser built at build time with Outlier
|
||||
// Detection builder pulled from gRPC LB Registry.
|
||||
return fmt.Errorf("config parser for Outlier Detection returned config with unexpected type %T: %v", lbCfg, lbCfg)
|
||||
}
|
||||
p.outlierDetection = *odCfg
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolverError handles errors reported by the xdsResolver.
|
||||
func (b *cdsBalancer) ResolverError(err error) {
|
||||
// Missing Listener or RouteConfiguration on the management server
|
||||
// results in a 'resource not found' error from the xDS resolver. In
|
||||
// these cases, we should report transient failure.
|
||||
if xdsresource.ErrType(err) == xdsresource.ErrorTypeResourceNotFound {
|
||||
b.closeChildPolicyAndReportTF(err)
|
||||
return
|
||||
}
|
||||
var root string
|
||||
if b.lbCfg != nil {
|
||||
root = b.lbCfg.ClusterName
|
||||
}
|
||||
b.onClusterError(root, err)
|
||||
}
|
||||
|
||||
// UpdateSubConnState handles subConn updates from gRPC.
|
||||
func (b *cdsBalancer) UpdateSubConnState(sc balancer.SubConn, state balancer.SubConnState) {
|
||||
b.logger.Errorf("UpdateSubConnState(%v, %+v) called unexpectedly", sc, state)
|
||||
}
|
||||
|
||||
// closeChildPolicyAndReportTF closes the child policy, if it exists, and
|
||||
// updates the connectivity state of the channel to TransientFailure with an
|
||||
// error picker.
|
||||
func (b *cdsBalancer) closeChildPolicyAndReportTF(err error) {
|
||||
if b.childLB != nil {
|
||||
b.childLB.Close()
|
||||
b.childLB = nil
|
||||
}
|
||||
b.cc.UpdateState(balancer.State{
|
||||
ConnectivityState: connectivity.TransientFailure,
|
||||
Picker: base.NewErrPicker(err),
|
||||
})
|
||||
}
|
||||
|
||||
// Close closes the child policy, unsubscribes to the dynamic cluster, and
|
||||
// closes the cdsBalancer.
|
||||
func (b *cdsBalancer) Close() {
|
||||
if b.childLB != nil {
|
||||
b.childLB.Close()
|
||||
b.childLB = nil
|
||||
}
|
||||
if b.unsubscribe != nil {
|
||||
b.unsubscribe()
|
||||
}
|
||||
b.logger.Infof("Shutdown")
|
||||
}
|
||||
|
||||
func (b *cdsBalancer) ExitIdle() {
|
||||
if b.childLB == nil {
|
||||
b.logger.Warningf("Received ExitIdle with no child policy")
|
||||
return
|
||||
}
|
||||
// This implementation assumes the child balancer supports
|
||||
// ExitIdle (but still checks for the interface's existence to
|
||||
// avoid a panic if not). If the child does not, no subconns
|
||||
// will be connected.
|
||||
b.childLB.ExitIdle()
|
||||
}
|
||||
|
||||
// Node ID needs to be manually added to errors generated in the following
|
||||
// scenarios:
|
||||
// - resource-does-not-exist: since the xDS watch API uses a separate callback
|
||||
// instead of returning an error value. TODO(gRFC A88): Once A88 is
|
||||
// implemented, the xDS client will be able to add the node ID to
|
||||
// resource-does-not-exist errors as well, and we can get rid of this
|
||||
// special handling.
|
||||
// - received a good update from the xDS client, but the update either contains
|
||||
// an invalid security configuration or contains invalid aggragate cluster
|
||||
// config.
|
||||
func (b *cdsBalancer) annotateErrorWithNodeID(err error) error {
|
||||
nodeID := b.xdsClient.BootstrapConfig().Node().GetId()
|
||||
return fmt.Errorf("[xDS node id: %v]: %w", nodeID, err)
|
||||
}
|
||||
|
||||
// onClusterAmbientError handles an ambient error, if a childLB already has a
|
||||
// good update, it should continue using that.
|
||||
func (b *cdsBalancer) onClusterAmbientError(name string, err error) {
|
||||
b.logger.Warningf("Cluster resource %q received ambient error update: %v", name, err)
|
||||
|
||||
if xdsresource.ErrType(err) != xdsresource.ErrorTypeConnection && b.childLB != nil {
|
||||
// Connection errors will be sent to the child balancers directly.
|
||||
// There's no need to forward them.
|
||||
b.childLB.ResolverError(err)
|
||||
}
|
||||
}
|
||||
|
||||
// onClusterResourceError handles errors to stop using the previously seen
|
||||
// resource. Propagates the error down to the child policy if one exists, and
|
||||
// puts the channel in TRANSIENT_FAILURE.
|
||||
func (b *cdsBalancer) onClusterResourceError(name string, err error) {
|
||||
b.logger.Warningf("CDS watch for resource %q reported resource error", name)
|
||||
b.closeChildPolicyAndReportTF(err)
|
||||
}
|
||||
|
||||
func (b *cdsBalancer) onClusterError(name string, err error) {
|
||||
if b.childLB != nil {
|
||||
b.onClusterAmbientError(name, err)
|
||||
} else {
|
||||
b.onClusterResourceError(name, err)
|
||||
}
|
||||
}
|
||||
1135
internal/xds/balancer/cdsbalancer/cdsbalancer_test.go
Normal file
1135
internal/xds/balancer/cdsbalancer/cdsbalancer_test.go
Normal file
File diff suppressed because it is too large
Load Diff
375
internal/xds/balancer/cdsbalancer/configbuilder.go
Normal file
375
internal/xds/balancer/cdsbalancer/configbuilder.go
Normal file
@@ -0,0 +1,375 @@
|
||||
/*
|
||||
*
|
||||
* 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 cdsbalancer
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"google.golang.org/grpc/experimental/balancer/hostname"
|
||||
"google.golang.org/grpc/experimental/balancer/weight"
|
||||
"google.golang.org/grpc/internal/envconfig"
|
||||
"google.golang.org/grpc/internal/hierarchy"
|
||||
"google.golang.org/grpc/internal/proxyattributes"
|
||||
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||||
xdsinternal "google.golang.org/grpc/internal/xds"
|
||||
"google.golang.org/grpc/internal/xds/balancer/clusterimpl"
|
||||
"google.golang.org/grpc/internal/xds/balancer/outlierdetection"
|
||||
"google.golang.org/grpc/internal/xds/balancer/priority"
|
||||
"google.golang.org/grpc/internal/xds/balancer/wrrlocality"
|
||||
"google.golang.org/grpc/internal/xds/clients"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
"google.golang.org/grpc/resolver"
|
||||
)
|
||||
|
||||
const million = 1000000
|
||||
|
||||
// priorityConfig is config for one priority. For example, if there's an EDS and
|
||||
// a DNS, the priority list will be [priorityConfig{EDS}, priorityConfig{DNS}].
|
||||
//
|
||||
// Each priorityConfig corresponds to one leaf cluster retrieved from XDSConfig
|
||||
// for the top-level cluster.
|
||||
type priorityConfig struct {
|
||||
// clusterConfig has the cluster update as well as EDS or DNS endpoints
|
||||
// depending on the leaf cluster type.
|
||||
clusterConfig *xdsresource.ClusterConfig
|
||||
// outlierDetection is the Outlier Detection LB configuration for this
|
||||
// priority.
|
||||
outlierDetection outlierdetection.LBConfig
|
||||
// Each leaf cluster has a name generator so that the child policies can
|
||||
// reuse names between updates (EDS updates for example).
|
||||
childNameGen *nameGenerator
|
||||
}
|
||||
|
||||
// hostName returns the name of the host for the given cluster.
|
||||
//
|
||||
// For EDS, it's the EDSServiceName (or ClusterName if empty).
|
||||
// For DNS, it's the DNSHostName.
|
||||
func hostName(clusterName string, update xdsresource.ClusterUpdate) string {
|
||||
switch update.ClusterType {
|
||||
case xdsresource.ClusterTypeEDS:
|
||||
if update.EDSServiceName != "" {
|
||||
return update.EDSServiceName
|
||||
}
|
||||
return clusterName
|
||||
case xdsresource.ClusterTypeLogicalDNS:
|
||||
return update.DNSHostName
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// buildPriorityConfigJSON builds balancer config for the passed in
|
||||
// priorities.
|
||||
//
|
||||
// The built tree of balancers (see test for the output struct).
|
||||
//
|
||||
// ┌────────┐
|
||||
// │priority│
|
||||
// └┬──────┬┘
|
||||
// │ │
|
||||
// ┌──────────▼─┐ ┌─▼──────────┐
|
||||
// │cluster_impl│ │cluster_impl│
|
||||
// └──────┬─────┘ └─────┬──────┘
|
||||
// │ │
|
||||
// ┌──────▼─────┐ ┌─────▼──────┐
|
||||
// │xDSLBPolicy │ │xDSLBPolicy │ (Locality and Endpoint picking layer)
|
||||
// └────────────┘ └────────────┘
|
||||
func buildPriorityConfigJSON(priorities []*priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]byte, []resolver.Endpoint, error) {
|
||||
pc, endpoints, err := buildPriorityConfig(priorities, xdsLBPolicy)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to build priority config: %v", err)
|
||||
}
|
||||
ret, err := json.Marshal(pc)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to marshal built priority config struct into json: %v", err)
|
||||
}
|
||||
return ret, endpoints, nil
|
||||
}
|
||||
|
||||
func buildPriorityConfig(priorities []*priorityConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*priority.LBConfig, []resolver.Endpoint, error) {
|
||||
var (
|
||||
retConfig = &priority.LBConfig{Children: make(map[string]*priority.Child)}
|
||||
retEndpoints []resolver.Endpoint
|
||||
)
|
||||
for _, p := range priorities {
|
||||
clusterUpdate := p.clusterConfig.Cluster
|
||||
switch clusterUpdate.ClusterType {
|
||||
case xdsresource.ClusterTypeEDS:
|
||||
names, configs, endpoints, err := buildClusterImplConfigForEDS(p.childNameGen, p.clusterConfig, xdsLBPolicy)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
retConfig.Priorities = append(retConfig.Priorities, names...)
|
||||
retEndpoints = append(retEndpoints, endpoints...)
|
||||
odCfgs := convertClusterImplMapToOutlierDetection(configs, p.outlierDetection)
|
||||
for n, c := range odCfgs {
|
||||
retConfig.Children[n] = &priority.Child{
|
||||
Config: &internalserviceconfig.BalancerConfig{Name: outlierdetection.Name, Config: c},
|
||||
// Ignore all re-resolution from EDS children.
|
||||
IgnoreReresolutionRequests: true,
|
||||
}
|
||||
}
|
||||
continue
|
||||
case xdsresource.ClusterTypeLogicalDNS:
|
||||
name, config, endpoints := buildClusterImplConfigForDNS(p.childNameGen, p.clusterConfig, xdsLBPolicy)
|
||||
retConfig.Priorities = append(retConfig.Priorities, name)
|
||||
retEndpoints = append(retEndpoints, endpoints...)
|
||||
odCfg := makeClusterImplOutlierDetectionChild(config, p.outlierDetection)
|
||||
retConfig.Children[name] = &priority.Child{
|
||||
Config: &internalserviceconfig.BalancerConfig{Name: outlierdetection.Name, Config: odCfg},
|
||||
// Not ignore re-resolution from DNS children, they will trigger
|
||||
// DNS to re-resolve.
|
||||
IgnoreReresolutionRequests: false,
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
return retConfig, retEndpoints, nil
|
||||
}
|
||||
|
||||
func convertClusterImplMapToOutlierDetection(ciCfgs map[string]*clusterimpl.LBConfig, odCfg outlierdetection.LBConfig) map[string]*outlierdetection.LBConfig {
|
||||
odCfgs := make(map[string]*outlierdetection.LBConfig, len(ciCfgs))
|
||||
for n, c := range ciCfgs {
|
||||
odCfgs[n] = makeClusterImplOutlierDetectionChild(c, odCfg)
|
||||
}
|
||||
return odCfgs
|
||||
}
|
||||
|
||||
func makeClusterImplOutlierDetectionChild(ciCfg *clusterimpl.LBConfig, odCfg outlierdetection.LBConfig) *outlierdetection.LBConfig {
|
||||
odCfgRet := odCfg
|
||||
odCfgRet.ChildPolicy = &internalserviceconfig.BalancerConfig{Name: clusterimpl.Name, Config: ciCfg}
|
||||
return &odCfgRet
|
||||
}
|
||||
|
||||
func buildClusterImplConfigForDNS(g *nameGenerator, config *xdsresource.ClusterConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) (string, *clusterimpl.LBConfig, []resolver.Endpoint) {
|
||||
pName := fmt.Sprintf("priority-%v", g.prefix)
|
||||
clusterUpdate := config.Cluster
|
||||
lbconfig := &clusterimpl.LBConfig{
|
||||
Cluster: clusterUpdate.ClusterName,
|
||||
ChildPolicy: xdsLBPolicy,
|
||||
}
|
||||
endpoints := config.EndpointConfig.DNSEndpoints.Endpoints
|
||||
if len(endpoints) == 0 {
|
||||
return pName, lbconfig, nil
|
||||
}
|
||||
var retEndpoint resolver.Endpoint
|
||||
for _, e := range endpoints {
|
||||
// LOGICAL_DNS requires all resolved addresses to be grouped into a
|
||||
// single logical endpoint. We iterate over the input endpoints and
|
||||
// aggregate their addresses into a new endpoint variable.
|
||||
retEndpoint.Addresses = append(retEndpoint.Addresses, e.Addresses...)
|
||||
}
|
||||
// Even though localities are not a thing for the LOGICAL_DNS cluster and
|
||||
// its endpoint(s), we add an empty locality attribute here to ensure that
|
||||
// LB policies that rely on locality information (like weighted_target)
|
||||
// continue to work.
|
||||
localityStr := xdsinternal.LocalityString(clients.Locality{})
|
||||
retEndpoint = hostname.Set(hierarchy.SetInEndpoint(retEndpoint, []string{pName, localityStr}), clusterUpdate.DNSHostName)
|
||||
// Set the locality weight to 1. This is required because the child policy
|
||||
// like weighted_target which relies on locality weights to distribute
|
||||
// traffic. These policies may drop traffic if the weight is 0.
|
||||
retEndpoint = wrrlocality.SetAddrInfo(retEndpoint, wrrlocality.AddrInfo{LocalityWeight: 1})
|
||||
return pName, lbconfig, []resolver.Endpoint{retEndpoint}
|
||||
}
|
||||
|
||||
// buildClusterImplConfigForEDS returns a list of cluster_impl configs, one for
|
||||
// each priority, sorted by priority, and the addresses for each priority (with
|
||||
// hierarchy attributes set).
|
||||
//
|
||||
// For example, if there are two priorities, the returned values will be
|
||||
// - ["p0", "p1"]
|
||||
// - map{"p0":p0_config, "p1":p1_config}
|
||||
// - [p0_address_0, p0_address_1, p1_address_0, p1_address_1]
|
||||
// - p0 addresses' hierarchy attributes are set to p0
|
||||
func buildClusterImplConfigForEDS(g *nameGenerator, config *xdsresource.ClusterConfig, xdsLBPolicy *internalserviceconfig.BalancerConfig) ([]string, map[string]*clusterimpl.LBConfig, []resolver.Endpoint, error) {
|
||||
edsUpdate := config.EndpointConfig.EDSUpdate
|
||||
|
||||
// Localities of length 0 is triggered by an NACK or resource-not-found
|
||||
// error before update, or an empty localities list in an update. In either
|
||||
// case want to create a priority, and send down empty address list, causing
|
||||
// TF for that priority. "If any discovery mechanism instance experiences an
|
||||
// error retrieving data, and it has not previously reported any results, it
|
||||
// should report a result that is a single priority with no endpoints." -
|
||||
// A37
|
||||
priorities := [][]xdsresource.Locality{{}}
|
||||
if len(edsUpdate.Localities) != 0 {
|
||||
priorities = groupLocalitiesByPriority(edsUpdate.Localities)
|
||||
}
|
||||
retNames := g.generate(priorities)
|
||||
retConfigs := make(map[string]*clusterimpl.LBConfig, len(retNames))
|
||||
var retEndpoints []resolver.Endpoint
|
||||
for i, pName := range retNames {
|
||||
priorityLocalities := priorities[i]
|
||||
cfg, endpoints, err := priorityLocalitiesToClusterImpl(priorityLocalities, pName, *config.Cluster, xdsLBPolicy)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
retConfigs[pName] = cfg
|
||||
retEndpoints = append(retEndpoints, endpoints...)
|
||||
}
|
||||
return retNames, retConfigs, retEndpoints, nil
|
||||
}
|
||||
|
||||
// groupLocalitiesByPriority returns the localities grouped by priority.
|
||||
//
|
||||
// The returned list is sorted from higher priority to lower. Each item in the
|
||||
// list is a group of localities.
|
||||
//
|
||||
// For example, for L0-p0, L1-p0, L2-p1, results will be
|
||||
// - [[L0, L1], [L2]]
|
||||
func groupLocalitiesByPriority(localities []xdsresource.Locality) [][]xdsresource.Locality {
|
||||
priorities := make(map[int][]xdsresource.Locality)
|
||||
for _, locality := range localities {
|
||||
priority := int(locality.Priority)
|
||||
priorities[priority] = append(priorities[priority], locality)
|
||||
}
|
||||
// Sort the priorities based on the int value, deduplicate, and then turn
|
||||
// the sorted list into a string list. This will be child names, in priority
|
||||
// order.
|
||||
priorityIntSlice := slices.Sorted(maps.Keys(priorities))
|
||||
ret := make([][]xdsresource.Locality, 0, len(priorityIntSlice))
|
||||
for _, p := range priorityIntSlice {
|
||||
ret = append(ret, priorities[p])
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
// priorityLocalitiesToClusterImpl takes a list of localities (with the same
|
||||
// priority), and generates a cluster impl policy config, and a list of
|
||||
// addresses with their path hierarchy set to [priority-name, locality-name], so
|
||||
// priority and the xDS LB Policy know which child policy each address is for.
|
||||
func priorityLocalitiesToClusterImpl(localities []xdsresource.Locality, priorityName string, clusterUpdate xdsresource.ClusterUpdate, xdsLBPolicy *internalserviceconfig.BalancerConfig) (*clusterimpl.LBConfig, []resolver.Endpoint, error) {
|
||||
var retEndpoints []resolver.Endpoint
|
||||
|
||||
// Compute the sum of locality weights to normalize locality weights. The
|
||||
// xDS client guarantees that the sum of locality weights (within a
|
||||
// priority) will not exceed MaxUint32.
|
||||
var localityWeightSum uint32
|
||||
for _, locality := range localities {
|
||||
localityWeightSum += locality.Weight
|
||||
}
|
||||
|
||||
for _, locality := range localities {
|
||||
// Compute the sum of endpoint weights to normalize endpoint weights.
|
||||
// The xDS client does not currently guarantee that the sum of endpoint
|
||||
// weights (within a locality) will not exceed MaxUint32. TODO(i/8862):
|
||||
// Once the xDS client guarantees that the sum of endpoint weights does
|
||||
// not exceed MaxUint32, we can change the type of this variable from
|
||||
// uint64 to uint32.
|
||||
var endpointWeightSum uint64
|
||||
for _, endpoint := range locality.Endpoints {
|
||||
endpointWeightSum += uint64(endpoint.Weight)
|
||||
}
|
||||
|
||||
localityStr := xdsinternal.LocalityString(locality.ID)
|
||||
for _, endpoint := range locality.Endpoints {
|
||||
// Filter out all "unhealthy" endpoints (unknown and healthy are
|
||||
// both considered to be healthy:
|
||||
// https://www.envoyproxy.io/docs/envoy/latest/api-v2/api/v2/core/health_check.proto#envoy-api-enum-core-healthstatus).
|
||||
if endpoint.HealthStatus != xdsresource.EndpointHealthStatusHealthy && endpoint.HealthStatus != xdsresource.EndpointHealthStatusUnknown {
|
||||
continue
|
||||
}
|
||||
|
||||
// Create a copy of endpoint.ResolverEndpoint to avoid race between
|
||||
// the xDS Client (which owns this shared object in its cache) and
|
||||
// the Cluster Resolver (which is trying to modify attributes).
|
||||
resolverEndpoint := endpoint.ResolverEndpoint
|
||||
resolverEndpoint.Addresses = slices.Clone(endpoint.ResolverEndpoint.Addresses)
|
||||
|
||||
if clusterUpdate.IsHTTP11ProxyEnabled {
|
||||
var proxyAddrStr string
|
||||
if val, ok := endpoint.Metadata["envoy.http11_proxy_transport_socket.proxy_address"].(xdsresource.ProxyAddressMetadataValue); ok {
|
||||
proxyAddrStr = val.Address
|
||||
} else if val, ok := locality.Metadata["envoy.http11_proxy_transport_socket.proxy_address"].(xdsresource.ProxyAddressMetadataValue); ok {
|
||||
proxyAddrStr = val.Address
|
||||
}
|
||||
if proxyAddrStr != "" {
|
||||
for idx, addr := range resolverEndpoint.Addresses {
|
||||
connectAddr := addr.Addr
|
||||
addr.Addr = proxyAddrStr
|
||||
resolverEndpoint.Addresses[idx] = proxyattributes.Set(addr, proxyattributes.Options{
|
||||
ConnectAddr: connectAddr,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resolverEndpoint = hierarchy.SetInEndpoint(resolverEndpoint, []string{priorityName, localityStr})
|
||||
resolverEndpoint = xdsinternal.SetLocalityIDInEndpoint(resolverEndpoint, locality.ID)
|
||||
// "To provide the xds_wrr_locality load balancer information about
|
||||
// locality weights received from EDS, the cluster resolver will
|
||||
// populate a new locality weight attribute for each address The
|
||||
// attribute will have the weight (as an integer) of the locality
|
||||
// the address is part of." - A52
|
||||
resolverEndpoint = wrrlocality.SetAddrInfo(resolverEndpoint, wrrlocality.AddrInfo{LocalityWeight: locality.Weight})
|
||||
|
||||
if envconfig.PickFirstWeightedShuffling {
|
||||
normalizedLocalityWeight := fractionToFixedPoint(uint64(locality.Weight), uint64(localityWeightSum))
|
||||
normalizedEndpointWeight := fractionToFixedPoint(uint64(endpoint.Weight), endpointWeightSum)
|
||||
endpointWeight := fixedPointMultiply(normalizedEndpointWeight, normalizedLocalityWeight)
|
||||
if endpointWeight == 0 {
|
||||
endpointWeight = 1
|
||||
}
|
||||
resolverEndpoint = weight.Set(resolverEndpoint, weight.EndpointInfo{Weight: endpointWeight})
|
||||
} else {
|
||||
resolverEndpoint = weight.Set(resolverEndpoint, weight.EndpointInfo{Weight: locality.Weight * endpoint.Weight})
|
||||
}
|
||||
retEndpoints = append(retEndpoints, resolverEndpoint)
|
||||
}
|
||||
}
|
||||
return &clusterimpl.LBConfig{
|
||||
Cluster: clusterUpdate.ClusterName,
|
||||
ChildPolicy: xdsLBPolicy,
|
||||
}, retEndpoints, nil
|
||||
}
|
||||
|
||||
// fixedPointFractionalBits is the number of bits used for the fractional part
|
||||
// of normalized endpoint and locality weights.
|
||||
//
|
||||
// We use the UQ1.31 fixed-point format (Unsigned, 1 integer bit, 31 fractional bits).
|
||||
// This allows representing values in the range [0.0, 2.0) with a precision
|
||||
// of 2^-31.
|
||||
//
|
||||
// Bit Layout:
|
||||
// [ 31 ] [ 30 ................. 0 ]
|
||||
//
|
||||
// | |
|
||||
// | +--- Fractional Part (31 bits)
|
||||
// +------------------ Integer Part (1 bit)
|
||||
//
|
||||
// See gRFC A113 for more details.
|
||||
const fixedPointFractionalBits = 31
|
||||
|
||||
// fractionToFixedPoint converts a fraction represented by numerator and
|
||||
// denominator to a fixed-point number between 0 and 1 represented as a uint32.
|
||||
//
|
||||
// The xDS client guarantees that the sum of locality weights (within a
|
||||
// priority) will not exceed MaxUint32. TODO(i/8862): Once the xDS client
|
||||
// guarantees that the sum of endpoint weights does not exceed MaxUint32, we can
|
||||
// change the types of this function's arguments from uint64 to uint32.
|
||||
func fractionToFixedPoint(numerator, denominator uint64) uint32 {
|
||||
return uint32(uint64(numerator) << fixedPointFractionalBits / uint64(denominator))
|
||||
}
|
||||
|
||||
func fixedPointMultiply(a, b uint32) uint32 {
|
||||
return uint32((uint64(a) * uint64(b)) >> fixedPointFractionalBits)
|
||||
}
|
||||
111
internal/xds/balancer/cdsbalancer/configbuilder_childname.go
Normal file
111
internal/xds/balancer/cdsbalancer/configbuilder_childname.go
Normal file
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 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 cdsbalancer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/internal/xds/clients"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
)
|
||||
|
||||
// nameGenerator generates a child name for a list of priorities (each priority
|
||||
// is a list of localities).
|
||||
//
|
||||
// The purpose of this generator is to reuse names between updates. So the
|
||||
// struct keeps state between generate() calls, and a later generate() might
|
||||
// return names returned by the previous call.
|
||||
type nameGenerator struct {
|
||||
prevLocalitiesToChildNames map[clients.Locality]string // locality to child name mapping generated for the previous update
|
||||
prevChildNames []string // prioritized list of child names generated for the previous update
|
||||
prefix uint64
|
||||
nextID uint64
|
||||
}
|
||||
|
||||
func newNameGenerator(prefix uint64) *nameGenerator {
|
||||
return &nameGenerator{prefix: prefix}
|
||||
}
|
||||
|
||||
// generate returns a list of names for the given list of priorities.
|
||||
//
|
||||
// Each priority is a list of localities. The name for the priority is picked as
|
||||
// - for each locality in this priority, if it exists in the existing names,
|
||||
// this priority will reuse the name
|
||||
// - if no reusable name is found for this priority, a new name is generated
|
||||
//
|
||||
// For example:
|
||||
// - update 1: [[L1], [L2], [L3]] --> ["0", "1", "2"]
|
||||
// - update 2: [[L1], [L2], [L3]] --> ["0", "1", "2"]
|
||||
// - update 3: [[L1, L2], [L3]] --> ["0", "2"] (Two priorities were merged)
|
||||
// - update 4: [[L1], [L4]] --> ["0", "3",] (A priority was split, and a new priority was added)
|
||||
func (ng *nameGenerator) generate(priorities [][]xdsresource.Locality) []string {
|
||||
ret := make([]string, len(priorities))
|
||||
usedNames := make(map[string]bool)
|
||||
newNames := make(map[clients.Locality]string)
|
||||
|
||||
// Pass 1: Same priority index match.
|
||||
for i, priority := range priorities {
|
||||
if i >= len(ng.prevChildNames) {
|
||||
continue
|
||||
}
|
||||
targetName := ng.prevChildNames[i]
|
||||
for _, locality := range priority {
|
||||
if name, ok := ng.prevLocalitiesToChildNames[locality.ID]; ok && name == targetName {
|
||||
ret[i] = targetName
|
||||
usedNames[targetName] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: Greedy reuse.
|
||||
for i, priority := range priorities {
|
||||
if ret[i] != "" {
|
||||
continue
|
||||
}
|
||||
for _, locality := range priority {
|
||||
if name, ok := ng.prevLocalitiesToChildNames[locality.ID]; ok {
|
||||
if !usedNames[name] {
|
||||
ret[i] = name
|
||||
usedNames[name] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 3: New name.
|
||||
for i, name := range ret {
|
||||
if name == "" {
|
||||
newID := fmt.Sprintf("priority-%d-%d", ng.prefix, ng.nextID)
|
||||
ng.nextID++
|
||||
ret[i] = newID
|
||||
usedNames[newID] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Update state.
|
||||
for i, priority := range priorities {
|
||||
for _, l := range priority {
|
||||
newNames[l.ID] = ret[i]
|
||||
}
|
||||
}
|
||||
ng.prevLocalitiesToChildNames = newNames
|
||||
ng.prevChildNames = ret
|
||||
return ret
|
||||
}
|
||||
@@ -0,0 +1,311 @@
|
||||
/*
|
||||
*
|
||||
* Copyright 2022 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 cdsbalancer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"google.golang.org/grpc/internal/xds/clients"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource"
|
||||
)
|
||||
|
||||
func (s) Test_nameGenerator_generate(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
prefix uint64
|
||||
steps []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}
|
||||
}{
|
||||
{
|
||||
name: "init, two new priorities",
|
||||
prefix: 3,
|
||||
steps: []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}},
|
||||
{{ID: clients.Locality{Zone: "L1"}}},
|
||||
},
|
||||
want: []string{"priority-3-0", "priority-3-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "one new priority",
|
||||
prefix: 1,
|
||||
steps: []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}},
|
||||
},
|
||||
want: []string{"priority-1-0"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}},
|
||||
{{ID: clients.Locality{Zone: "L1"}}},
|
||||
},
|
||||
want: []string{"priority-1-0", "priority-1-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge two priorities",
|
||||
prefix: 4,
|
||||
steps: []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}},
|
||||
{{ID: clients.Locality{Zone: "L1"}}},
|
||||
{{ID: clients.Locality{Zone: "L2"}}},
|
||||
},
|
||||
want: []string{"priority-4-0", "priority-4-1", "priority-4-2"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
|
||||
{{ID: clients.Locality{Zone: "L2"}}},
|
||||
},
|
||||
want: []string{"priority-4-0", "priority-4-2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "swap two priorities",
|
||||
steps: []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}},
|
||||
{{ID: clients.Locality{Zone: "L1"}}},
|
||||
{{ID: clients.Locality{Zone: "L2"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1", "priority-0-2"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L1"}}},
|
||||
{{ID: clients.Locality{Zone: "L0"}}},
|
||||
{{ID: clients.Locality{Zone: "L2"}}},
|
||||
},
|
||||
want: []string{"priority-0-1", "priority-0-0", "priority-0-2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "split priority",
|
||||
steps: []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
|
||||
{{ID: clients.Locality{Zone: "L2"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}},
|
||||
{{ID: clients.Locality{Zone: "L1"}}},
|
||||
{{ID: clients.Locality{Zone: "L2"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-2", "priority-0-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "priority index preference",
|
||||
steps: []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}},
|
||||
{{ID: clients.Locality{Zone: "L1"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L1"}}, {ID: clients.Locality{Zone: "L0"}}},
|
||||
},
|
||||
want: []string{"priority-0-0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "merge partial",
|
||||
steps: []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
|
||||
{{ID: clients.Locality{Zone: "L2"}}, {ID: clients.Locality{Zone: "L3"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}, {ID: clients.Locality{Zone: "L2"}}},
|
||||
{{ID: clients.Locality{Zone: "L3"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
|
||||
{{ID: clients.Locality{Zone: "L2"}}, {ID: clients.Locality{Zone: "L3"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "swap shift",
|
||||
steps: []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
|
||||
{{ID: clients.Locality{Zone: "L2"}}, {ID: clients.Locality{Zone: "L3"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L2"}}},
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
|
||||
},
|
||||
want: []string{"priority-0-1", "priority-0-0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "replace priority",
|
||||
steps: []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
|
||||
{{ID: clients.Locality{Zone: "L2"}}, {ID: clients.Locality{Zone: "L3"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
|
||||
{{ID: clients.Locality{Zone: "L5"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "reordered merge",
|
||||
steps: []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L1"}}, {ID: clients.Locality{Zone: "L2"}}},
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L3"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}, {ID: clients.Locality{Zone: "L2"}}},
|
||||
{{ID: clients.Locality{Zone: "L3"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L1"}}, {ID: clients.Locality{Zone: "L2"}}},
|
||||
{{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L3"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "three-step shift stability",
|
||||
steps: []struct {
|
||||
input [][]xdsresource.Locality
|
||||
want []string
|
||||
}{
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}},
|
||||
{{ID: clients.Locality{Zone: "L1"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L2"}}},
|
||||
{{ID: clients.Locality{Zone: "L0"}}},
|
||||
{{ID: clients.Locality{Zone: "L1"}}},
|
||||
},
|
||||
want: []string{"priority-0-2", "priority-0-0", "priority-0-1"},
|
||||
},
|
||||
{
|
||||
input: [][]xdsresource.Locality{
|
||||
{{ID: clients.Locality{Zone: "L0"}}},
|
||||
{{ID: clients.Locality{Zone: "L1"}}},
|
||||
},
|
||||
want: []string{"priority-0-0", "priority-0-1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ng := newNameGenerator(tt.prefix)
|
||||
for i, step := range tt.steps {
|
||||
got := ng.generate(step.input)
|
||||
if diff := cmp.Diff(got, step.want); diff != "" {
|
||||
t.Errorf("step %d: generate() = got: %v, want: %v, diff (-got +want): %s", i, got, step.want, diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
1445
internal/xds/balancer/cdsbalancer/configbuilder_test.go
Normal file
1445
internal/xds/balancer/cdsbalancer/configbuilder_test.go
Normal file
File diff suppressed because it is too large
Load Diff
1467
internal/xds/balancer/cdsbalancer/e2e_test/aggregate_cluster_test.go
Normal file
1467
internal/xds/balancer/cdsbalancer/e2e_test/aggregate_cluster_test.go
Normal file
File diff suppressed because it is too large
Load Diff
420
internal/xds/balancer/cdsbalancer/e2e_test/balancer_test.go
Normal file
420
internal/xds/balancer/cdsbalancer/e2e_test/balancer_test.go
Normal file
@@ -0,0 +1,420 @@
|
||||
/*
|
||||
* 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 e2e_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/balancer"
|
||||
"google.golang.org/grpc/balancer/roundrobin"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/connectivity"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/balancer/stub"
|
||||
iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/internal/testutils"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/internal/xds/balancer/clusterimpl"
|
||||
"google.golang.org/grpc/internal/xds/balancer/outlierdetection"
|
||||
"google.golang.org/grpc/internal/xds/balancer/priority"
|
||||
"google.golang.org/grpc/internal/xds/balancer/wrrlocality"
|
||||
"google.golang.org/grpc/internal/xds/xdsclient/xdsresource/version"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/serviceconfig"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/durationpb"
|
||||
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||
|
||||
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||
v3corepb "github.com/envoyproxy/go-control-plane/envoy/config/core/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"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
|
||||
_ "google.golang.org/grpc/internal/xds/balancer/cdsbalancer" // Register the "cds_experimental" LB policy.
|
||||
_ "google.golang.org/grpc/internal/xds/httpfilter/router" // Register the router filter
|
||||
_ "google.golang.org/grpc/internal/xds/resolver" // Register the xds resolver
|
||||
)
|
||||
|
||||
// setupAndDial performs common setup across all tests
|
||||
//
|
||||
// - creates an xDS client with the passed in bootstrap contents
|
||||
// - creates a xds resolver to be used
|
||||
// - creates a ClientConn to talk to the test backends
|
||||
//
|
||||
// Returns a function to close the ClientConn and the xDS client.
|
||||
func setupAndDial(t *testing.T, bootstrapContents []byte) (*grpc.ClientConn, func()) {
|
||||
t.Helper()
|
||||
// Create an xDS resolver with the above bootstrap configuration.
|
||||
r, err := internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("xDS resolver creation failed: %v", err)
|
||||
}
|
||||
// Create a ClientConn and make a successful RPC.
|
||||
cc, err := grpc.NewClient("xds:///"+serviceName, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(r))
|
||||
if err != nil {
|
||||
t.Fatalf("grpc.NewClient() failed: %v", err)
|
||||
}
|
||||
cc.Connect()
|
||||
return cc, func() {
|
||||
cc.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// TestErrorFromParentLB_ConnectionError tests the case where a connection error
|
||||
// is sent by the CDS LB Policy. The CDS LB policy sends a connection error when
|
||||
// the ADS stream to the management server breaks. The test verifies that there
|
||||
// is no perceivable effect because of this connection error, and that RPCs
|
||||
// continue to work (because the LB policies are expected to use previously
|
||||
// received xDS resources).
|
||||
func (s) TestErrorFromParentLB_ConnectionError(t *testing.T) {
|
||||
// Create a listener to be used by the management server. The test will
|
||||
// close this listener to simulate ADS stream breakage.
|
||||
lis, err := testutils.LocalTCPListener()
|
||||
if err != nil {
|
||||
t.Fatalf("testutils.LocalTCPListener() failed: %v", err)
|
||||
}
|
||||
|
||||
// Start an xDS management server with the above restartable listener, and
|
||||
// push a channel when the stream is closed.
|
||||
streamClosedCh := make(chan struct{}, 1)
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
||||
Listener: lis,
|
||||
OnStreamClosed: func(int64, *v3corepb.Node) {
|
||||
select {
|
||||
case streamClosedCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
server := stubserver.StartTestService(t, nil)
|
||||
defer server.Stop()
|
||||
|
||||
resources := e2e.DefaultClientResources(e2e.ResourceParams{
|
||||
NodeID: nodeID,
|
||||
DialTarget: serviceName,
|
||||
Host: "localhost",
|
||||
Port: testutils.ParsePort(t, server.Address),
|
||||
})
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create xDS client, xdsResolver, and dial the test backends.
|
||||
cc, cleanup := setupAndDial(t, bootstrapContents)
|
||||
defer cleanup()
|
||||
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
|
||||
// Close the listener and ensure that the ADS stream breaks.
|
||||
lis.Close()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout when waiting for ADS stream to close")
|
||||
default:
|
||||
}
|
||||
|
||||
// Ensure that RPCs continue to succeed for the next second.
|
||||
for end := time.Now().Add(time.Second); time.Now().Before(end); <-time.After(defaultTestShortTimeout) {
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestErrorFromParentLB_ResourceNotFound tests the case where a
|
||||
// resource-not-found error is received by the CDS LB policy for a cluster. The
|
||||
// resource-not-found error is received when the cluster resource associated
|
||||
// with these LB policies is removed by the management server. The test verifies
|
||||
// that the associated EDS is canceled and RPCs fail. It also ensures that when
|
||||
// the Cluster resource is added back, the EDS resource is re-requested and RPCs
|
||||
// being to succeed.
|
||||
func (s) TestErrorFromParentLB_ResourceNotFound(t *testing.T) {
|
||||
// Start an xDS management server that uses a couple of channels to
|
||||
// notify the test about the following events:
|
||||
// - an EDS requested with the expected resource name is requested
|
||||
// - EDS resource is unrequested, i.e, an EDS request with no resource name
|
||||
// is received, which indicates that we are no longer interested in that
|
||||
// resource.
|
||||
edsResourceRequestedCh := make(chan struct{}, 1)
|
||||
edsResourceCanceledCh := make(chan struct{}, 1)
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
|
||||
OnStreamRequest: func(_ int64, req *v3discoverypb.DiscoveryRequest) error {
|
||||
if req.GetTypeUrl() == version.V3EndpointsURL {
|
||||
switch len(req.GetResourceNames()) {
|
||||
case 0:
|
||||
select {
|
||||
case edsResourceCanceledCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
case 1:
|
||||
if req.GetResourceNames()[0] == edsServiceName {
|
||||
select {
|
||||
case edsResourceRequestedCh <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
default:
|
||||
t.Errorf("Unexpected number of resources, %d, in an EDS request", len(req.GetResourceNames()))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
server := stubserver.StartTestService(t, nil)
|
||||
defer server.Stop()
|
||||
|
||||
// Configure cluster and endpoints resources in the management server.
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{e2e.DefaultCluster(clusterName, edsServiceName, e2e.SecurityLevelNone)},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsServiceName, "localhost", []uint32{testutils.ParsePort(t, server.Address)})},
|
||||
SkipValidation: true,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create xDS client, xdsResolver, and dial the test backends.
|
||||
cc, cleanup := setupAndDial(t, bootstrapContents)
|
||||
defer cleanup()
|
||||
|
||||
// Wait for the EDS resource to be requested.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout when waiting for EDS resource to be requested")
|
||||
case <-edsResourceRequestedCh:
|
||||
}
|
||||
|
||||
// Ensure that a successful RPC can be made.
|
||||
client := testgrpc.NewTestServiceClient(cc)
|
||||
if _, err := client.EmptyCall(ctx, &testpb.Empty{}); err != nil {
|
||||
t.Fatalf("EmptyCall() failed: %v", err)
|
||||
}
|
||||
|
||||
oldCluster := resources.Clusters
|
||||
// Delete the cluster resource from the management server.
|
||||
resources.Clusters = nil
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for the EDS resource to be not requested anymore.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout when waiting for EDS resource to not be requested")
|
||||
case <-edsResourceCanceledCh:
|
||||
}
|
||||
|
||||
// Ensure that RPCs start to fail with expected error.
|
||||
wantErr := fmt.Sprintf("resource %q of type %q has been removed", clusterName, "ClusterResource")
|
||||
for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) {
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
_, err := client.EmptyCall(sCtx, &testpb.Empty{})
|
||||
if status.Code(err) == codes.Unavailable && strings.Contains(err.Error(), wantErr) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
t.Logf("EmptyCall failed: %v", err)
|
||||
}
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("RPCs did not fail after removal of Cluster resource")
|
||||
}
|
||||
|
||||
testutils.AwaitState(ctx, t, cc, connectivity.TransientFailure)
|
||||
|
||||
// Add the cluster resource back to the management server.
|
||||
resources.Clusters = oldCluster
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Wait for the EDS resource to be requested again.
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
t.Fatal("Timeout when waiting for EDS resource to be requested")
|
||||
case <-edsResourceRequestedCh:
|
||||
}
|
||||
|
||||
// Ensure that a successful RPC can be made.
|
||||
for ; ctx.Err() == nil; <-time.After(defaultTestShortTimeout) {
|
||||
sCtx, sCancel := context.WithTimeout(ctx, defaultTestShortTimeout)
|
||||
defer sCancel()
|
||||
if _, err := client.EmptyCall(sCtx, &testpb.Empty{}); err != nil {
|
||||
t.Logf("EmptyCall failed: %v", err)
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("RPCs did not fail after removal of Cluster resource")
|
||||
}
|
||||
}
|
||||
|
||||
// Test verifies that when the received Cluster resource contains outlier
|
||||
// detection configuration, the LB config pushed to the priority policy contains
|
||||
// the appropriate configuration for the outlier detection LB policy.
|
||||
func (s) TestOutlierDetectionConfigPropagationToChildPolicy(t *testing.T) {
|
||||
// Unregister the priority balancer builder for the duration of this test,
|
||||
// and register a policy under the same name that makes the LB config
|
||||
// pushed to it available to the test.
|
||||
priorityBuilder := balancer.Get(priority.Name)
|
||||
internal.BalancerUnregister(priorityBuilder.Name())
|
||||
lbCfgCh := make(chan serviceconfig.LoadBalancingConfig, 1)
|
||||
stub.Register(priority.Name, stub.BalancerFuncs{
|
||||
Init: func(bd *stub.BalancerData) {
|
||||
bd.ChildBalancer = priorityBuilder.Build(bd.ClientConn, bd.BuildOptions)
|
||||
},
|
||||
ParseConfig: func(lbCfg json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
|
||||
return priorityBuilder.(balancer.ConfigParser).ParseConfig(lbCfg)
|
||||
},
|
||||
UpdateClientConnState: func(bd *stub.BalancerData, ccs balancer.ClientConnState) error {
|
||||
select {
|
||||
case lbCfgCh <- ccs.BalancerConfig:
|
||||
default:
|
||||
}
|
||||
return bd.ChildBalancer.UpdateClientConnState(ccs)
|
||||
},
|
||||
Close: func(bd *stub.BalancerData) {
|
||||
bd.ChildBalancer.Close()
|
||||
},
|
||||
})
|
||||
defer balancer.Register(priorityBuilder)
|
||||
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
|
||||
server := stubserver.StartTestService(t, nil)
|
||||
defer server.Stop()
|
||||
|
||||
// Configure cluster and endpoints resources in the management server.
|
||||
cluster := e2e.DefaultCluster(clusterName, edsServiceName, e2e.SecurityLevelNone)
|
||||
cluster.OutlierDetection = &v3clusterpb.OutlierDetection{
|
||||
Interval: durationpb.New(10 * time.Second),
|
||||
BaseEjectionTime: durationpb.New(30 * time.Second),
|
||||
MaxEjectionTime: durationpb.New(300 * time.Second),
|
||||
MaxEjectionPercent: wrapperspb.UInt32(10),
|
||||
SuccessRateStdevFactor: wrapperspb.UInt32(2000),
|
||||
EnforcingSuccessRate: wrapperspb.UInt32(50),
|
||||
SuccessRateMinimumHosts: wrapperspb.UInt32(10),
|
||||
SuccessRateRequestVolume: wrapperspb.UInt32(50),
|
||||
}
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, routeName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(routeName, serviceName, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{cluster},
|
||||
Endpoints: []*v3endpointpb.ClusterLoadAssignment{e2e.DefaultEndpoint(edsServiceName, "localhost", []uint32{testutils.ParsePort(t, server.Address)})},
|
||||
SkipValidation: true,
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create xDS client, xdsResolver, and dial the test backends.
|
||||
_, cleanup := setupAndDial(t, bootstrapContents)
|
||||
defer cleanup()
|
||||
|
||||
// The priority configuration generated should have Outlier Detection as a
|
||||
// direct child due to Outlier Detection being turned on.
|
||||
wantCfg := &priority.LBConfig{
|
||||
Children: map[string]*priority.Child{
|
||||
"priority-0-0": {
|
||||
Config: &iserviceconfig.BalancerConfig{
|
||||
Name: outlierdetection.Name,
|
||||
Config: &outlierdetection.LBConfig{
|
||||
Interval: iserviceconfig.Duration(10 * time.Second), // default interval
|
||||
BaseEjectionTime: iserviceconfig.Duration(30 * time.Second),
|
||||
MaxEjectionTime: iserviceconfig.Duration(300 * time.Second),
|
||||
MaxEjectionPercent: 10,
|
||||
SuccessRateEjection: &outlierdetection.SuccessRateEjection{
|
||||
StdevFactor: 2000,
|
||||
EnforcementPercentage: 50,
|
||||
MinimumHosts: 10,
|
||||
RequestVolume: 50,
|
||||
},
|
||||
ChildPolicy: &iserviceconfig.BalancerConfig{
|
||||
Name: clusterimpl.Name,
|
||||
Config: &clusterimpl.LBConfig{
|
||||
Cluster: clusterName,
|
||||
ChildPolicy: &iserviceconfig.BalancerConfig{
|
||||
Name: wrrlocality.Name,
|
||||
Config: &wrrlocality.LBConfig{
|
||||
ChildPolicy: &iserviceconfig.BalancerConfig{
|
||||
Name: roundrobin.Name,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
IgnoreReresolutionRequests: true,
|
||||
},
|
||||
},
|
||||
Priorities: []string{"priority-0-0"},
|
||||
}
|
||||
|
||||
select {
|
||||
case lbCfg := <-lbCfgCh:
|
||||
gotCfg := lbCfg.(*priority.LBConfig)
|
||||
if diff := cmp.Diff(wantCfg, gotCfg); diff != "" {
|
||||
t.Fatalf("Child policy received unexpected diff in config (-want +got):\n%s", diff)
|
||||
}
|
||||
case <-ctx.Done():
|
||||
t.Fatalf("Timeout when waiting for child policy to receive its configuration")
|
||||
}
|
||||
}
|
||||
136
internal/xds/balancer/cdsbalancer/e2e_test/dns_impl_test.go
Normal file
136
internal/xds/balancer/cdsbalancer/e2e_test/dns_impl_test.go
Normal file
@@ -0,0 +1,136 @@
|
||||
/*
|
||||
* Copyright 2026 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 e2e_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
"google.golang.org/grpc/internal"
|
||||
"google.golang.org/grpc/internal/stubserver"
|
||||
"google.golang.org/grpc/internal/testutils/xds/e2e"
|
||||
"google.golang.org/grpc/peer"
|
||||
"google.golang.org/grpc/resolver"
|
||||
"google.golang.org/grpc/resolver/manual"
|
||||
|
||||
v3clusterpb "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
||||
v3listenerpb "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
||||
v3routepb "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
||||
testgrpc "google.golang.org/grpc/interop/grpc_testing"
|
||||
testpb "google.golang.org/grpc/interop/grpc_testing"
|
||||
|
||||
_ "google.golang.org/grpc/internal/xds/balancer/priority" // Register priority LB policy.
|
||||
)
|
||||
|
||||
// TestLogicalDNS_MultipleEndpoints tests the priority LB policy using a
|
||||
// LOGICAL_DNS discovery mechanism.
|
||||
//
|
||||
// The test verifies that multiple addresses returned by the DNS resolver are
|
||||
// grouped into a single endpoint (as per gRFC A61). Because the round_robin LB
|
||||
// policy sees only one endpoint, it should not rotate traffic between the
|
||||
// addresses. Instead, the single endpoint is picked, and connects to the first
|
||||
// address.
|
||||
func (s) TestLogicalDNS_MultipleEndpoints(t *testing.T) {
|
||||
// Spin up a management server to receive xDS resources from.
|
||||
managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{})
|
||||
|
||||
// Create bootstrap configuration pointing to the above management server.
|
||||
nodeID := uuid.New().String()
|
||||
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
|
||||
resolverBuilder, err := internal.NewXDSResolverWithConfigForTesting.(func([]byte) (resolver.Builder, error))(bootstrapContents)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create xDS resolver for testing: %v", err)
|
||||
}
|
||||
|
||||
// Start backend servers which provide an implementation of the TestService.
|
||||
server1 := stubserver.StartTestService(t, nil)
|
||||
defer server1.Stop()
|
||||
server2 := stubserver.StartTestService(t, nil)
|
||||
defer server2.Stop()
|
||||
|
||||
// Register a manual resolver with the "dns" scheme to override DNS resolution.
|
||||
// This global override is safe because connection to the xDS management
|
||||
// server uses the passthrough scheme instead and therefore overriding
|
||||
// the DNS resolver does not affect it in any way.
|
||||
const dnsScheme = "dns"
|
||||
dnsR := manual.NewBuilderWithScheme(dnsScheme)
|
||||
originalDNS := resolver.Get("dns")
|
||||
resolver.Register(dnsR)
|
||||
t.Cleanup(func() { resolver.Register(originalDNS) })
|
||||
|
||||
// For LOGICAL_DNS, this updates the SINGLE endpoint to have 2 IPs.
|
||||
dnsR.InitialState(resolver.State{
|
||||
Endpoints: []resolver.Endpoint{{
|
||||
Addresses: []resolver.Address{
|
||||
{Addr: server1.Address},
|
||||
{Addr: server2.Address},
|
||||
}}},
|
||||
})
|
||||
|
||||
const (
|
||||
serviceName = "test-xds-service"
|
||||
clusterName = "cluster-test-xds-service"
|
||||
endpointsName = "endpoints-test-xds-service"
|
||||
rdsName = "route-test-xds-service"
|
||||
)
|
||||
|
||||
resources := e2e.UpdateOptions{
|
||||
NodeID: nodeID,
|
||||
Listeners: []*v3listenerpb.Listener{e2e.DefaultClientListener(serviceName, rdsName)},
|
||||
Routes: []*v3routepb.RouteConfiguration{e2e.DefaultRouteConfig(rdsName, serviceName, clusterName)},
|
||||
Clusters: []*v3clusterpb.Cluster{e2e.ClusterResourceWithOptions(e2e.ClusterOptions{
|
||||
ClusterName: clusterName,
|
||||
ServiceName: endpointsName,
|
||||
Type: e2e.ClusterTypeLogicalDNS,
|
||||
DNSHostName: "dns",
|
||||
DNSPort: uint32(8080),
|
||||
Policy: e2e.LoadBalancingPolicyRoundRobin,
|
||||
})},
|
||||
Endpoints: nil,
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
|
||||
defer cancel()
|
||||
|
||||
if err := managementServer.Update(ctx, resources); err != nil {
|
||||
t.Fatalf("Failed to update management server: %v", err)
|
||||
}
|
||||
|
||||
// Create a ClientConn and make a successful RPC.
|
||||
cc, err := grpc.NewClient(fmt.Sprintf("xds:///"+serviceName), grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithResolvers(resolverBuilder))
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create new client for local test server: %v", err)
|
||||
}
|
||||
defer cc.Close()
|
||||
|
||||
// Ensure the RPC is routed to the first backend.
|
||||
testClient := testgrpc.NewTestServiceClient(cc)
|
||||
for i := 0; i < 10; i++ {
|
||||
var peer peer.Peer
|
||||
if _, err := testClient.EmptyCall(ctx, &testpb.Empty{}, grpc.Peer(&peer)); err != nil {
|
||||
t.Fatalf("RPC failed: %v", err)
|
||||
}
|
||||
|
||||
if got, want := peer.Addr.String(), server1.Address; got != want {
|
||||
t.Errorf("peer.Addr = %q, want = %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
1105
internal/xds/balancer/cdsbalancer/e2e_test/eds_impl_test.go
Normal file
1105
internal/xds/balancer/cdsbalancer/e2e_test/eds_impl_test.go
Normal file
File diff suppressed because it is too large
Load Diff
34
internal/xds/balancer/cdsbalancer/logging.go
Normal file
34
internal/xds/balancer/cdsbalancer/logging.go
Normal 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 cdsbalancer
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/grpc/grpclog"
|
||||
internalgrpclog "google.golang.org/grpc/internal/grpclog"
|
||||
)
|
||||
|
||||
const prefix = "[cds-lb %p] "
|
||||
|
||||
var logger = grpclog.Component("xds")
|
||||
|
||||
func prefixLogger(p *cdsBalancer) *internalgrpclog.PrefixLogger {
|
||||
return internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf(prefix, p))
|
||||
}
|
||||
Reference in New Issue
Block a user