priority: implement changes to child policy caching specified in A115 (#8997)
Summary of changes:
- Add a new API to the `balancergroup` that allows removal of child
policies with immediate effect, even when a cache is configured.
- Make the `priority` LB use this new API when removing child policies
that have been removed from its configuration.
- This change is protected with an environment variable that turns on
the new behavior by default.
- Update the name generation algorithm to give higher preference to
reuse names of existing priority (if there is a locality match) over
lower priorities.
- Fix a bunch of tests in the `priority` LB policy to ensure that they
work with the *old* and *new* behavior.
- Fix a couple of aggregate cluster tests such that they don't
continuously handle the same NACKed resource from the management server.
- Other minor cleanups in tests that I had to look at as part of making
this change.
RELEASE NOTES:
* xds/priority: Stop caching child LB policies removed from the
configuration. This will help reduce memory and cpu usage when
localities are constantly switching between priorities.
diff --git a/balancer/weightedtarget/weightedtarget_test.go b/balancer/weightedtarget/weightedtarget_test.go
index d78db53..8468892 100644
--- a/balancer/weightedtarget/weightedtarget_test.go
+++ b/balancer/weightedtarget/weightedtarget_test.go
@@ -1348,7 +1348,7 @@
return ret
}
-const initIdleBalancerName = "test-init-Idle-balancer"
+const initIdleBalancerName = "test-init-idle-balancer"
var errTestInitIdle = fmt.Errorf("init Idle balancer error 0")
@@ -1388,7 +1388,7 @@
"targets": {
"cluster_1": {
"weight":1,
- "childPolicy": [{"test-init-Idle-balancer": ""}]
+ "childPolicy": [{"test-init-idle-balancer": ""}]
}
}
}`))
diff --git a/internal/balancergroup/balancergroup.go b/internal/balancergroup/balancergroup.go
index cd1a9ed..8acb942 100644
--- a/internal/balancergroup/balancergroup.go
+++ b/internal/balancergroup/balancergroup.go
@@ -338,6 +338,16 @@
// closed after timeout. Cleanup work (closing sub-balancer and removing
// subconns) will be done after timeout.
func (bg *BalancerGroup) Remove(id string) {
+ bg.removeInternal(id, true)
+}
+
+// RemoveImmediately removes and closes the balancer with id from the group
+// immediately.
+func (bg *BalancerGroup) RemoveImmediately(id string) {
+ bg.removeInternal(id, false)
+}
+
+func (bg *BalancerGroup) removeInternal(id string, withCaching bool) {
bg.logger.Infof("Removing child policy for child %q", id)
bg.outgoingMu.Lock()
@@ -356,32 +366,40 @@
// Unconditionally remove the sub-balancer config from the map.
delete(bg.idToBalancerConfig, id)
- if bg.deletedBalancerCache != nil {
- if bg.logger.V(2) {
- bg.logger.Infof("Adding child policy for child %q to the balancer cache", id)
- bg.logger.Infof("Number of items remaining in the balancer cache: %d", bg.deletedBalancerCache.Len())
- }
-
- bg.deletedBalancerCache.Add(id, sbToRemove, func() {
+ if withCaching {
+ if bg.deletedBalancerCache != nil {
if bg.logger.V(2) {
- bg.logger.Infof("Removing child policy for child %q from the balancer cache after timeout", id)
+ bg.logger.Infof("Adding child policy for child %q to the balancer cache", id)
+ }
+ bg.deletedBalancerCache.Add(id, sbToRemove, func() {
+ if bg.logger.V(2) {
+ bg.logger.Infof("Removing child policy for child %q from the balancer cache after timeout", id)
+ bg.logger.Infof("Number of items remaining in the balancer cache: %d", bg.deletedBalancerCache.Len())
+ }
+
+ // A sub-balancer evicted from the timeout cache needs to closed
+ // and its subConns need to removed, unconditionally. There is a
+ // possibility that a sub-balancer might be removed (thereby
+ // moving it to the cache) around the same time that the
+ // balancergroup is closed, and by the time we get here the
+ // balancergroup might be closed. Check for `outgoingStarted ==
+ // true` at that point can lead to a leaked sub-balancer.
+ bg.outgoingMu.Lock()
+ sbToRemove.stopBalancer()
+ bg.outgoingMu.Unlock()
+ bg.cleanupSubConns(sbToRemove)
+ })
+ if bg.logger.V(2) {
bg.logger.Infof("Number of items remaining in the balancer cache: %d", bg.deletedBalancerCache.Len())
}
-
- // A sub-balancer evicted from the timeout cache needs to closed
- // and its subConns need to removed, unconditionally. There is a
- // possibility that a sub-balancer might be removed (thereby
- // moving it to the cache) around the same time that the
- // balancergroup is closed, and by the time we get here the
- // balancergroup might be closed. Check for `outgoingStarted ==
- // true` at that point can lead to a leaked sub-balancer.
- bg.outgoingMu.Lock()
- sbToRemove.stopBalancer()
bg.outgoingMu.Unlock()
- bg.cleanupSubConns(sbToRemove)
- })
- bg.outgoingMu.Unlock()
- return
+ return
+ }
+
+ // Fall through to remove the sub-balancer with immediate effect if we are not caching.
+ if bg.logger.V(2) {
+ bg.logger.Infof("Child policy for child %q was requested to be cached before eventual removal. No such cache exists. Removing right away.", id)
+ }
}
// Remove the sub-balancer with immediate effect if we are not caching.
@@ -481,7 +499,7 @@
// from map. Delete sc from the map only when state changes to Shutdown. Since
// it's just forwarding the action, there's no need for a removeSubConn()
// wrapper function.
-func (bg *BalancerGroup) newSubConn(config *subBalancerWrapper, addrs []resolver.Address, opts balancer.NewSubConnOptions) (balancer.SubConn, error) {
+func (bg *BalancerGroup) newSubConn(sbw *subBalancerWrapper, addrs []resolver.Address, opts balancer.NewSubConnOptions) (balancer.SubConn, error) {
// NOTE: if balancer with id was already removed, this should also return
// error. But since we call balancer.stopBalancer when removing the balancer, this
// shouldn't happen.
@@ -493,12 +511,12 @@
var sc balancer.SubConn
oldListener := opts.StateListener
opts.StateListener = func(state balancer.SubConnState) { bg.updateSubConnState(sc, state, oldListener) }
- sc, err := bg.cc.NewSubConn(addrs, opts)
+ sc, err := sbw.ClientConn.NewSubConn(addrs, opts)
if err != nil {
bg.incomingMu.Unlock()
return nil, err
}
- bg.scToSubBalancer[sc] = config
+ bg.scToSubBalancer[sc] = sbw
bg.incomingMu.Unlock()
return sc, nil
}
diff --git a/internal/balancergroup/balancergroup_ext_test.go b/internal/balancergroup/balancergroup_ext_test.go
new file mode 100644
index 0000000..bf53d99
--- /dev/null
+++ b/internal/balancergroup/balancergroup_ext_test.go
@@ -0,0 +1,124 @@
+/*
+ * 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 balancergroup_test
+
+import (
+ "context"
+ "strings"
+ "testing"
+ "time"
+
+ "google.golang.org/grpc/balancer"
+ "google.golang.org/grpc/internal/balancer/stub"
+ "google.golang.org/grpc/internal/balancergroup"
+ "google.golang.org/grpc/internal/grpctest"
+ "google.golang.org/grpc/internal/testutils"
+)
+
+type s struct {
+ grpctest.Tester
+}
+
+func Test(t *testing.T) {
+ grpctest.RunSubTests(t, s{})
+}
+
+const (
+ defaultTestTimeout = 5 * time.Second
+ defaultTestShortTimeout = 100 * time.Millisecond
+)
+
+// Tests verifies that the RemoveImmediately method of balancergroup closes the
+// child balancer immediately, while the Remove method does not close the child
+// balancer immediately, when the cache is enabled.
+func (s) TestBalancerGroup_RemoveImmediately(t *testing.T) {
+ // Channels to track child balancer creation and closure.
+ childLBCreated := make(chan string, 1)
+ childLBClosed := make(chan string, 1)
+
+ childLBName1 := strings.ToLower(t.Name()) + "-child-1"
+ t.Logf("Registering a child balancer with name %q", childLBName1)
+ stub.Register(childLBName1, stub.BalancerFuncs{
+ Init: func(*stub.BalancerData) {
+ childLBCreated <- childLBName1
+ },
+ Close: func(*stub.BalancerData) {
+ childLBClosed <- childLBName1
+ },
+ })
+
+ childLBName2 := strings.ToLower(t.Name()) + "-child-2"
+ t.Logf("Registering a child balancer with name %q", childLBName2)
+ stub.Register(childLBName2, stub.BalancerFuncs{
+ Init: func(*stub.BalancerData) {
+ childLBCreated <- childLBName2
+ },
+ Close: func(*stub.BalancerData) {
+ childLBClosed <- childLBName2
+ },
+ })
+
+ t.Logf("Creating a balancergroup with cache enabled")
+ tcc := testutils.NewBalancerClientConn(t)
+ bg := balancergroup.New(balancergroup.Options{
+ CC: tcc,
+ BuildOpts: balancer.BuildOptions{},
+ SubBalancerCloseTimeout: defaultTestTimeout,
+ })
+
+ t.Logf("Adding a child balancer with name %q to the group", "child-1")
+ bg.AddWithClientConn("child-1", childLBName1, tcc)
+ select {
+ case <-childLBCreated:
+ case <-time.After(defaultTestTimeout):
+ t.Fatalf("Timeout when waiting for child LB to be created")
+ }
+
+ t.Logf("Adding a child balancer with name %q to the group", "child-2")
+ bg.AddWithClientConn("child-2", childLBName2, tcc)
+ select {
+ case <-childLBCreated:
+ case <-time.After(defaultTestTimeout):
+ t.Fatalf("Timeout when waiting for child LB to be created")
+ }
+
+ t.Logf("Removing the child balancer with name %q from the group with immediate effect", "child-1")
+ bg.RemoveImmediately("child-1")
+ select {
+ case <-childLBClosed:
+ case <-time.After(defaultTestTimeout):
+ t.Fatalf("Timeout when waiting for child LB to be closed")
+ }
+
+ t.Logf("Removing the child balancer with name %q from the group with caching", "child-2")
+ bg.Remove("child-2")
+ sCtx, sCancel := context.WithTimeout(context.Background(), defaultTestShortTimeout)
+ defer sCancel()
+ select {
+ case <-childLBClosed:
+ t.Fatalf("Child LB closed when expected to be cached")
+ case <-sCtx.Done():
+ }
+
+ t.Logf("Closing the balancergroup, which should close the second child balancer")
+ bg.Close()
+ select {
+ case <-childLBClosed:
+ case <-time.After(defaultTestTimeout):
+ t.Fatalf("Timeout when waiting for child LB to be closed")
+ }
+}
diff --git a/internal/envconfig/envconfig.go b/internal/envconfig/envconfig.go
index e3adb91..3ae45fa 100644
--- a/internal/envconfig/envconfig.go
+++ b/internal/envconfig/envconfig.go
@@ -119,6 +119,13 @@
// A future release will remove this environment variable, enabling strict
// path checking behavior unconditionally.
DisableStrictPathChecking = boolFromEnv("GRPC_GO_EXPERIMENTAL_DISABLE_STRICT_PATH_CHECKING", false)
+
+ // EnablePriorityLBChildPolicyCache controls whether the priority balancer
+ // should cache child balancers that are removed from the LB policy config,
+ // for a period of 15 minutes. This is disabled by default, but can be
+ // enabled by setting the env variable
+ // GRPC_EXPERIMENTAL_ENABLE_PRIORITY_LB_CHILD_POLICY_CACHE to true.
+ EnablePriorityLBChildPolicyCache = boolFromEnv("GRPC_EXPERIMENTAL_ENABLE_PRIORITY_LB_CHILD_POLICY_CACHE", false)
)
func boolFromEnv(envVar string, def bool) bool {
diff --git a/internal/xds/balancer/cdsbalancer/configbuilder_childname.go b/internal/xds/balancer/cdsbalancer/configbuilder_childname.go
index 8f118d4..2b0fedd 100644
--- a/internal/xds/balancer/cdsbalancer/configbuilder_childname.go
+++ b/internal/xds/balancer/cdsbalancer/configbuilder_childname.go
@@ -31,9 +31,10 @@
// struct keeps state between generate() calls, and a later generate() might
// return names returned by the previous call.
type nameGenerator struct {
- existingNames map[clients.Locality]string
- prefix uint64
- nextID uint64
+ 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 {
@@ -53,36 +54,58 @@
// - 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 {
- var ret []string
+ ret := make([]string, len(priorities))
usedNames := make(map[string]bool)
newNames := make(map[clients.Locality]string)
- for _, priority := range priorities {
- var nameFound 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.existingNames[locality.ID]; ok {
+ 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] {
- nameFound = name
- // Found a name to use. No need to process the remaining
- // localities.
+ ret[i] = name
+ usedNames[name] = true
break
}
}
}
-
- if nameFound == "" {
- // No appropriate used name is found. Make a new name.
- nameFound = fmt.Sprintf("priority-%d-%d", ng.prefix, ng.nextID)
- ng.nextID++
- }
-
- ret = append(ret, nameFound)
- // All localities in this priority share the same name. Add them all to
- // the new map.
- for _, l := range priority {
- newNames[l.ID] = nameFound
- }
- usedNames[nameFound] = true
}
- ng.existingNames = newNames
+
+ // 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
}
diff --git a/internal/xds/balancer/cdsbalancer/configbuilder_childname_test.go b/internal/xds/balancer/cdsbalancer/configbuilder_childname_test.go
index e534d54..7275f55 100644
--- a/internal/xds/balancer/cdsbalancer/configbuilder_childname_test.go
+++ b/internal/xds/balancer/cdsbalancer/configbuilder_childname_test.go
@@ -29,82 +29,282 @@
tests := []struct {
name string
prefix uint64
- input1 [][]xdsresource.Locality
- input2 [][]xdsresource.Locality
- want []string
+ steps []struct {
+ input [][]xdsresource.Locality
+ want []string
+ }
}{
{
name: "init, two new priorities",
prefix: 3,
- input1: nil,
- input2: [][]xdsresource.Locality{
- {{ID: clients.Locality{Zone: "L0"}}},
- {{ID: clients.Locality{Zone: "L1"}}},
+ 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"},
+ },
},
- want: []string{"priority-3-0", "priority-3-1"},
},
{
name: "one new priority",
prefix: 1,
- input1: [][]xdsresource.Locality{
- {{ID: clients.Locality{Zone: "L0"}}},
+ 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"},
+ },
},
- input2: [][]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,
- input1: [][]xdsresource.Locality{
- {{ID: clients.Locality{Zone: "L0"}}},
- {{ID: clients.Locality{Zone: "L1"}}},
- {{ID: clients.Locality{Zone: "L2"}}},
+ 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"},
+ },
},
- input2: [][]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",
- input1: [][]xdsresource.Locality{
- {{ID: clients.Locality{Zone: "L0"}}},
- {{ID: clients.Locality{Zone: "L1"}}},
- {{ID: clients.Locality{Zone: "L2"}}},
+ 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"},
+ },
},
- input2: [][]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",
- input1: [][]xdsresource.Locality{
- {{ID: clients.Locality{Zone: "L0"}}, {ID: clients.Locality{Zone: "L1"}}},
- {{ID: clients.Locality{Zone: "L2"}}},
+ 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"},
+ },
},
- input2: [][]xdsresource.Locality{
- {{ID: clients.Locality{Zone: "L0"}}},
- {{ID: clients.Locality{Zone: "L1"}}}, // This gets a newly generated name, since "0-0" was already picked.
- {{ID: clients.Locality{Zone: "L2"}}},
+ },
+ {
+ 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"},
+ },
},
- want: []string{"priority-0-0", "priority-0-2", "priority-0-1"},
+ },
+ {
+ 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)
- got1 := ng.generate(tt.input1)
- t.Logf("%v", got1)
- got := ng.generate(tt.input2)
- if diff := cmp.Diff(got, tt.want); diff != "" {
- t.Errorf("generate() = got: %v, want: %v, diff (-got +want): %s", got, tt.want, diff)
+ 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)
+ }
}
})
}
diff --git a/internal/xds/balancer/cdsbalancer/e2e_test/aggregate_cluster_test.go b/internal/xds/balancer/cdsbalancer/e2e_test/aggregate_cluster_test.go
index 76de30a..8b75dbe 100644
--- a/internal/xds/balancer/cdsbalancer/e2e_test/aggregate_cluster_test.go
+++ b/internal/xds/balancer/cdsbalancer/e2e_test/aggregate_cluster_test.go
@@ -23,6 +23,7 @@
"slices"
"strconv"
"strings"
+ "sync/atomic"
"testing"
"time"
@@ -697,8 +698,8 @@
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
// Start two test backends.
- servers, cleanup3 := startTestServiceBackends(t, 2)
- defer cleanup3()
+ servers, cleanup := startTestServiceBackends(t, 2)
+ defer cleanup()
addrs, _ := backendAddressesAndPorts(t, servers)
// Configure an aggregate cluster pointing to an EDS and LOGICAL_DNS
@@ -795,8 +796,28 @@
// the DNS resolver pushes an update, the test verifies that we switch to the
// DNS cluster and can make a successful RPC.
func (s) TestAggregateCluster_BadEDSFromError_GoodToBadDNS(t *testing.T) {
- // Start an xDS management server.
- managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
+ // Start an xDS management server and block it from continuously re-sending
+ // the same EDS response that triggers a NACK. This is to ensure that the
+ // test can proceed to the point of verifying that we fall back to the
+ // LOGICAL_DNS cluster. More more details see:
+ // https://github.com/grpc/grpc-go/issues/8994.
+ ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
+ defer cancel()
+ edsResponseSeen := atomic.Bool{}
+ managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
+ OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) {
+ if resp.GetTypeUrl() != version.V3EndpointsURL {
+ return
+ }
+ if edsResponseSeen.Load() {
+ t.Logf("Received EDS response again, blocking to prevent test from busylooping on the same EDS response: %v", resp)
+ <-ctx.Done()
+ return
+ }
+ edsResponseSeen.Store(true)
+ },
+ AllowResourceSubset: true,
+ })
// Create bootstrap configuration pointing to the above management server.
nodeID := uuid.New().String()
@@ -834,8 +855,6 @@
Endpoints: []*v3endpointpb.ClusterLoadAssignment{nackEndpointResource},
SkipValidation: true,
}
- ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
- defer cancel()
if err := managementServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
@@ -1093,8 +1112,29 @@
// the LOGICAL_DNS cluster, because it is supposed to treat the bad EDS response
// as though it received an update with no endpoints.
func (s) TestAggregateCluster_Fallback_EDSNackedWithoutPreviousGoodUpdate(t *testing.T) {
- // Start an xDS management server.
- managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{AllowResourceSubset: true})
+ ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
+ defer cancel()
+
+ // Start an xDS management server and block it from continuously re-sending
+ // the same EDS response that triggers a NACK. This is to ensure that the
+ // test can proceed to the point of verifying that we fall back to the
+ // LOGICAL_DNS cluster. More more details see:
+ // https://github.com/grpc/grpc-go/issues/8994.
+ edsResponseSeen := atomic.Bool{}
+ managementServer := e2e.StartManagementServer(t, e2e.ManagementServerOptions{
+ OnStreamResponse: func(_ context.Context, _ int64, _ *v3discoverypb.DiscoveryRequest, resp *v3discoverypb.DiscoveryResponse) {
+ if resp.GetTypeUrl() != version.V3EndpointsURL {
+ return
+ }
+ if edsResponseSeen.Load() {
+ t.Logf("Received EDS response again, blocking to prevent test from busylooping on the same EDS response: %v", resp)
+ <-ctx.Done()
+ return
+ }
+ edsResponseSeen.Store(true)
+ },
+ AllowResourceSubset: true,
+ })
// Create bootstrap configuration pointing to the above management server.
nodeID := uuid.New().String()
@@ -1103,8 +1143,8 @@
// Start two test backends and extract their host and port. The first
// backend is used for the EDS cluster and the second backend is used for
// the LOGICAL_DNS cluster.
- servers, cleanup3 := startTestServiceBackends(t, 2)
- defer cleanup3()
+ servers, cleanup := startTestServiceBackends(t, 2)
+ defer cleanup()
addrs, ports := backendAddressesAndPorts(t, servers)
dnsHostName, dnsPort := hostAndPortFromAddress(t, servers[1].Address)
@@ -1128,11 +1168,7 @@
// Set a load balancing weight of 0 for the backend in the EDS resource.
// This is expected to be NACKed by the xDS client. Since the
- // cluster_resolver LB policy has no previously received good EDS resource,
- // it will treat this as though it received an update with no endpoints.
resources.Endpoints[0].Endpoints[0].LbEndpoints[0].LoadBalancingWeight = &wrapperspb.UInt32Value{Value: 0}
- ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
- defer cancel()
if err := managementServer.Update(ctx, resources); err != nil {
t.Fatal(err)
}
diff --git a/internal/xds/balancer/cdsbalancer/e2e_test/eds_impl_test.go b/internal/xds/balancer/cdsbalancer/e2e_test/eds_impl_test.go
index a92a2ac..ee45ca3 100644
--- a/internal/xds/balancer/cdsbalancer/e2e_test/eds_impl_test.go
+++ b/internal/xds/balancer/cdsbalancer/e2e_test/eds_impl_test.go
@@ -236,8 +236,8 @@
bootstrapContents := e2e.DefaultBootstrapContents(t, nodeID, managementServer.Address)
// Start backend servers which provide an implementation of the TestService.
- servers, cleanup2 := startTestServiceBackends(t, 4)
- defer cleanup2()
+ servers, cleanup := startTestServiceBackends(t, 4)
+ defer cleanup()
addrs, ports := backendAddressesAndPorts(t, servers)
// Create xDS resources for consumption by the test. We start off with two
diff --git a/internal/xds/balancer/clustermanager/clustermanager_test.go b/internal/xds/balancer/clustermanager/clustermanager_test.go
index 17a3026..576f440 100644
--- a/internal/xds/balancer/clustermanager/clustermanager_test.go
+++ b/internal/xds/balancer/clustermanager/clustermanager_test.go
@@ -513,7 +513,7 @@
}
}
-const initIdleBalancerName = "test-init-Idle-balancer"
+const initIdleBalancerName = "test-init-idle-balancer"
var errTestInitIdle = fmt.Errorf("init Idle balancer error 0")
@@ -551,7 +551,7 @@
configJSON1 := `{
"children": {
- "cds:cluster_1":{ "childPolicy": [{"test-init-Idle-balancer":""}] }
+ "cds:cluster_1":{ "childPolicy": [{"test-init-idle-balancer":""}] }
}
}`
config1, err := parser.ParseConfig([]byte(configJSON1))
diff --git a/internal/xds/balancer/priority/balancer.go b/internal/xds/balancer/priority/balancer.go
index 950cd13..e842920 100644
--- a/internal/xds/balancer/priority/balancer.go
+++ b/internal/xds/balancer/priority/balancer.go
@@ -34,6 +34,7 @@
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/internal/balancergroup"
"google.golang.org/grpc/internal/buffer"
+ "google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpclog"
"google.golang.org/grpc/internal/grpcsync"
"google.golang.org/grpc/internal/hierarchy"
@@ -153,7 +154,7 @@
// The balancing policy name is changed, close the old child. But don't
// rebuild, rebuild will happen when syncing priorities.
if currentChild.balancerName != bb.Name() {
- currentChild.stop()
+ currentChild.stop(true)
currentChild.updateBalancerName(bb.Name())
}
@@ -169,7 +170,7 @@
// Cleanup resources used by children removed from the config.
for name, oldChild := range b.children {
if _, ok := newConfig.Children[name]; !ok {
- oldChild.stop()
+ oldChild.stop(!envconfig.EnablePriorityLBChildPolicyCache)
delete(b.children, name)
}
}
@@ -230,7 +231,7 @@
// Stop the child policies, this is necessary to stop the init timers in the
// children.
for _, child := range b.children {
- child.stop()
+ child.stop(true)
}
}
diff --git a/internal/xds/balancer/priority/balancer_child.go b/internal/xds/balancer/priority/balancer_child.go
index 5e76391..ad5b5a8 100644
--- a/internal/xds/balancer/priority/balancer_child.go
+++ b/internal/xds/balancer/priority/balancer_child.go
@@ -133,12 +133,16 @@
// It doesn't do it directly. It asks the balancer group to remove it.
//
// Note that the underlying balancer group could keep the child in a cache.
-func (cb *childBalancer) stop() {
+func (cb *childBalancer) stop(immediate bool) {
if !cb.started {
return
}
cb.stopInitTimer()
- cb.parent.bg.Remove(cb.name)
+ if immediate {
+ cb.parent.bg.RemoveImmediately(cb.name)
+ } else {
+ cb.parent.bg.Remove(cb.name)
+ }
cb.started = false
cb.state = balancer.State{
ConnectivityState: connectivity.Connecting,
diff --git a/internal/xds/balancer/priority/balancer_priority.go b/internal/xds/balancer/priority/balancer_priority.go
index a57df7d..3181ce2 100644
--- a/internal/xds/balancer/priority/balancer_priority.go
+++ b/internal/xds/balancer/priority/balancer_priority.go
@@ -128,7 +128,7 @@
b.logger.Warningf("Priority name %q is not found in list of child policies", name)
continue
}
- child.stop()
+ child.stop(false)
}
}
diff --git a/internal/xds/balancer/priority/balancer_test.go b/internal/xds/balancer/priority/balancer_test.go
index 070e95c..c1c1572 100644
--- a/internal/xds/balancer/priority/balancer_test.go
+++ b/internal/xds/balancer/priority/balancer_test.go
@@ -29,6 +29,7 @@
"google.golang.org/grpc/balancer/roundrobin"
"google.golang.org/grpc/connectivity"
"google.golang.org/grpc/internal/balancer/stub"
+ "google.golang.org/grpc/internal/envconfig"
"google.golang.org/grpc/internal/grpctest"
"google.golang.org/grpc/internal/hierarchy"
internalserviceconfig "google.golang.org/grpc/internal/serviceconfig"
@@ -68,9 +69,6 @@
for i := 0; i < testBackendAddrsCount; i++ {
testBackendAddrStrs = append(testBackendAddrStrs, fmt.Sprintf("%d.%d.%d.%d:%d", i, i, i, i, i))
}
- // Disable sub-balancer caching for all but the tests which exercise the
- // caching behavior.
- DefaultSubBalancerCloseTimeout = time.Duration(0)
balancer.Register(&anotherRR{Builder: balancer.Get(roundrobin.Name)})
}
@@ -85,6 +83,17 @@
//
// Init 0 and 1; 0 is up, use 0; add 2, use 0; remove 2, use 0.
func (s) TestPriority_HighPriorityReady(t *testing.T) {
+ t.Run("caching_enabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, true)
+ testPriorityHighPriorityReady(t)
+ })
+ t.Run("caching_disabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, false)
+ testPriorityHighPriorityReady(t)
+ })
+}
+
+func testPriorityHighPriorityReady(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
@@ -199,6 +208,17 @@
// Init 0 and 1; 0 is up, use 0; 0 is down, 1 is up, use 1; add 2, use 1; 1 is
// down, use 2; remove 2, use 1.
func (s) TestPriority_SwitchPriority(t *testing.T) {
+ t.Run("caching_enabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, true)
+ testPrioritySwitchPriority(t)
+ })
+ t.Run("caching_disabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, false)
+ testPrioritySwitchPriority(t)
+ })
+}
+
+func testPrioritySwitchPriority(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
@@ -340,16 +360,19 @@
t.Fatalf("failed to update ClientConn state: %v", err)
}
- // p2 SubConns are shut down.
- scToShutdown := <-cc.ShutdownSubConnCh
- // The same SubConn is closed by gracefulswitch and pickfirstleaf when they
- // are closed. Remove duplicate events.
- // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
- // workaround once pickfirst is the only leaf policy and responsible for
- // shutting down SubConns.
- <-cc.ShutdownSubConnCh
- if scToShutdown != sc2 {
- t.Fatalf("ShutdownSubConn, want %v, got %v", sc2, scToShutdown)
+ // p2 will be closed immediately only if the cache is not enabled.
+ if !envconfig.EnablePriorityLBChildPolicyCache {
+ // p2 SubConns are shut down.
+ scToShutdown := <-cc.ShutdownSubConnCh
+ // The same SubConn is closed by gracefulswitch and pickfirstleaf when
+ // they are closed. Remove duplicate events.
+ // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
+ // workaround once pickfirst is the only leaf policy and responsible for
+ // shutting down SubConns.
+ <-cc.ShutdownSubConnCh
+ if scToShutdown != sc2 {
+ t.Fatalf("ShutdownSubConn, want %v, got %v", sc2, scToShutdown)
+ }
}
// Should get an update with 1's old transient failure picker, to override
@@ -374,6 +397,10 @@
//
// Init 0 and 1; 0 is up, use 0; 0 is connecting, 1 is up, use 1; 0 is ready,
// use 0.
+//
+// Env var GRPC_EXPERIMENTAL_ENABLE_PRIORITY_LB_CHILD_POLICY_CACHE does not
+// affect whether a lower priority child policy is cached when it is removed
+// from the balancer group.
func (s) TestPriority_HighPriorityToConnectingFromReady(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
@@ -383,7 +410,7 @@
pb := bb.Build(cc, balancer.BuildOptions{})
defer pb.Close()
- // Two localities, with priorities [0, 1], each with one backend.
+ t.Log("Two localities, with priorities [0, 1], each with one backend.")
if err := pb.UpdateClientConnState(balancer.ClientConnState{
ResolverState: resolver.State{
Endpoints: []resolver.Endpoint{
@@ -408,7 +435,7 @@
}
sc0 := <-cc.NewSubConnCh
- // p0 is ready.
+ t.Log("Make p0 ready.")
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
@@ -417,16 +444,15 @@
t.Fatal(err.Error())
}
- // Turn 0 to TransientFailure, will start and use 1.
+ t.Log("Turn down 0, will start and use 1.")
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.TransientFailure})
-
// Before 1 gets READY, picker should return NoSubConnAvailable, so RPCs
// will retry.
if err := cc.WaitForPickerWithErr(ctx, balancer.ErrNoSubConnAvailable); err != nil {
t.Fatal(err.Error())
}
- // Handle SubConn creation from 1.
+ t.Log("Handle SubConn creation from 1.")
addrs1 := <-cc.NewSubConnAddrsCh
if got, want := addrs1[0].Addr, testBackendAddrStrs[1]; got != want {
t.Fatalf("sc is created with addr %v, want %v", got, want)
@@ -440,22 +466,15 @@
t.Fatal(err.Error())
}
- // Turn 0 back to Ready.
+ t.Logf("Turn 0 back to Ready.")
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
- // p1 subconn should be shut down.
- scToShutdown := <-cc.ShutdownSubConnCh
- // The same SubConn is closed by gracefulswitch and pickfirstleaf when they
- // are closed. Remove duplicate events.
- // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
- // workaround once pickfirst is the only leaf policy and responsible for
- // shutting down SubConns.
- <-cc.ShutdownSubConnCh
- if scToShutdown != sc1 {
- t.Fatalf("ShutdownSubConn, want %v, got %v", sc0, scToShutdown)
- }
-
+ // At this point p1 is removed from the balancer group, but it gets cached.
+ // Therefore its subchannels will not be removed immediately. So, we do not
+ // check for the shutdown of p1's subchannels here. Instead, we check that
+ // p0 is used again after it becomes ready, which indicates that the
+ // priority switch is successful.
if err := cc.WaitForRoundRobinPicker(ctx, sc0); err != nil {
t.Fatal(err.Error())
}
@@ -646,25 +665,6 @@
// When 0 becomes ready, 0 should be used, 1 and 2 should all be closed.
sc0.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
- // sc1 and sc2 should be shut down.
- //
- // With localities caching, the lower priorities are closed after a timeout,
- // in goroutines. The order is no longer guaranteed.
- // The same SubConn is closed by gracefulswitch and pickfirstleaf when they
- // are closed. Remove duplicate events.
- // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
- // workaround once pickfirst is the only leaf policy and responsible for
- // shutting down SubConns.
- scToShutdown := [2]balancer.SubConn{}
- scToShutdown[0] = <-cc.ShutdownSubConnCh
- <-cc.ShutdownSubConnCh
- scToShutdown[1] = <-cc.ShutdownSubConnCh
- <-cc.ShutdownSubConnCh
-
- if !(scToShutdown[0] == sc1 && scToShutdown[1] == sc2) && !(scToShutdown[0] == sc2 && scToShutdown[1] == sc1) {
- t.Errorf("ShutdownSubConn, want [%v, %v], got %v", sc1, sc2, scToShutdown)
- }
-
// Test pick with 0.
if err := cc.WaitForRoundRobinPicker(ctx, sc0); err != nil {
t.Fatal(err.Error())
@@ -741,6 +741,17 @@
// EDS removes all priorities, and re-adds them.
func (s) TestPriority_RemovesAllPriorities(t *testing.T) {
+ t.Run("caching_enabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, true)
+ testPriorityRemovesAllPriorities(t)
+ })
+ t.Run("caching_disabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, false)
+ testPriorityRemovesAllPriorities(t)
+ })
+}
+
+func testPriorityRemovesAllPriorities(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
@@ -797,16 +808,19 @@
t.Fatalf("failed to update ClientConn state: %v", err)
}
- // p0 subconn should be shut down.
- scToShutdown := <-cc.ShutdownSubConnCh
- // The same SubConn is closed by gracefulswitch and pickfirstleaf when they
- // are closed. Remove duplicate events.
- // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
- // workaround once pickfirst is the only leaf policy and responsible for
- // shutting down SubConns.
- <-cc.ShutdownSubConnCh
- if scToShutdown != sc0 {
- t.Fatalf("ShutdownSubConn, want %v, got %v", sc0, scToShutdown)
+ // p0 will be closed immediately only if the cache is not enabled.
+ if !envconfig.EnablePriorityLBChildPolicyCache {
+ // p0 SubConns are shut down.
+ scToShutdown := <-cc.ShutdownSubConnCh
+ // The same SubConn is closed by gracefulswitch and pickfirstleaf when
+ // they are closed. Remove duplicate events.
+ // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
+ // workaround once pickfirst is the only leaf policy and responsible for
+ // shutting down SubConns.
+ <-cc.ShutdownSubConnCh
+ if scToShutdown != sc0 {
+ t.Fatalf("ShutdownSubConn, want %v, got %v", sc0, scToShutdown)
+ }
}
// Test pick return TransientFailure.
@@ -839,6 +853,14 @@
}
sc01 := <-cc.NewSubConnCh
+ if envconfig.EnablePriorityLBChildPolicyCache {
+ // The old p0 SubConn is shut down.
+ scToShutdown := <-cc.ShutdownSubConnCh
+ if scToShutdown != sc0 {
+ t.Fatalf("ShutdownSubConn, want %v, got %v", sc0, scToShutdown)
+ }
+ }
+
// Don't send any update to p0, so to not override the old state of p0.
// Later, connect to p1 and then remove p1. This will fallback to p0, and
// will send p0's old picker if they are not correctly removed.
@@ -874,16 +896,19 @@
t.Fatalf("failed to update ClientConn state: %v", err)
}
- // p1 subconn should be shut down.
- scToShutdown1 := <-cc.ShutdownSubConnCh
- // The same SubConn is closed by gracefulswitch and pickfirstleaf when they
- // are closed. Remove duplicate events.
- // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
- // workaround once pickfirst is the only leaf policy and responsible for
- // shutting down SubConns.
- <-cc.ShutdownSubConnCh
- if scToShutdown1 != sc11 {
- t.Fatalf("ShutdownSubConn, want %v, got %v", sc11, scToShutdown1)
+ // p1 will be closed immediately only if the cache is not enabled.
+ if !envconfig.EnablePriorityLBChildPolicyCache {
+ // p1 SubConns are shut down.
+ scToShutdown := <-cc.ShutdownSubConnCh
+ // The same SubConn is closed by gracefulswitch and pickfirstleaf when
+ // they are closed. Remove duplicate events.
+ // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
+ // workaround once pickfirst is the only leaf policy and responsible for
+ // shutting down SubConns.
+ <-cc.ShutdownSubConnCh
+ if scToShutdown != sc11 {
+ t.Fatalf("ShutdownSubConn, want %v, got %v", sc11, scToShutdown)
+ }
}
// Test pick return NoSubConn.
@@ -1004,8 +1029,19 @@
// Test the case where the first and only priority is removed.
func (s) TestPriority_FirstPriorityUnavailable(t *testing.T) {
- const testPriorityInitTimeout = 200 * time.Millisecond
- overrideInitTimeout(t, testPriorityInitTimeout)
+ t.Run("caching_enabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, true)
+ testPriorityFirstPriorityUnavailable(t)
+ })
+ t.Run("caching_disabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, false)
+ testPriorityFirstPriorityUnavailable(t)
+ })
+}
+
+func testPriorityFirstPriorityUnavailable(t *testing.T) {
+ ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
+ defer cancel()
cc := testutils.NewBalancerClientConn(t)
bb := balancer.Get(Name)
@@ -1029,6 +1065,12 @@
t.Fatalf("failed to update ClientConn state: %v", err)
}
+ addrs0 := <-cc.NewSubConnAddrsCh
+ if got, want := addrs0[0].Addr, testBackendAddrStrs[0]; got != want {
+ t.Fatalf("sc is created with addr %v, want %v", got, want)
+ }
+ sc0 := <-cc.NewSubConnCh
+
// Remove the only localities.
if err := pb.UpdateClientConnState(balancer.ClientConnState{
ResolverState: resolver.State{
@@ -1042,14 +1084,42 @@
t.Fatalf("failed to update ClientConn state: %v", err)
}
- // Wait after double the init timer timeout, to ensure it doesn't panic.
- time.Sleep(testPriorityInitTimeout * 2)
+ // p0 will be closed immediately only if the cache is not enabled.
+ if !envconfig.EnablePriorityLBChildPolicyCache {
+ // p0 SubConns are shut down.
+ scToShutdown := <-cc.ShutdownSubConnCh
+ // The same SubConn is closed by gracefulswitch and pickfirstleaf when
+ // they are closed. Remove duplicate events.
+ // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
+ // workaround once pickfirst is the only leaf policy and responsible for
+ // shutting down SubConns.
+ <-cc.ShutdownSubConnCh
+ if scToShutdown != sc0 {
+ t.Fatalf("ShutdownSubConn, want %v, got %v", sc0, scToShutdown)
+ }
+ }
+
+ // Test pick return TransientFailure.
+ if err := cc.WaitForPickerWithErr(ctx, ErrAllPrioritiesRemoved); err != nil {
+ t.Fatal(err.Error())
+ }
}
// When a child is moved from low priority to high.
//
// Init a(p0) and b(p1); a(p0) is up, use a; move b to p0, a to p1, use b.
func (s) TestPriority_MoveChildToHigherPriority(t *testing.T) {
+ t.Run("caching_enabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, true)
+ testPriorityMoveChildToHigherPriority(t)
+ })
+ t.Run("caching_disabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, false)
+ testPriorityMoveChildToHigherPriority(t)
+ })
+}
+
+func testPriorityMoveChildToHigherPriority(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
@@ -1120,18 +1190,6 @@
t.Fatal(err.Error())
}
- // Old subconn should be shut down.
- scToShutdown := <-cc.ShutdownSubConnCh
- // The same SubConn is closed by gracefulswitch and pickfirstleaf when they
- // are closed. Remove duplicate events.
- // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
- // workaround once pickfirst is the only leaf policy and responsible for
- // shutting down SubConns.
- <-cc.ShutdownSubConnCh
- if scToShutdown != sc1 {
- t.Fatalf("ShutdownSubConn, want %v, got %v", sc1, scToShutdown)
- }
-
addrs2 := <-cc.NewSubConnAddrsCh
if got, want := addrs2[0].Addr, testBackendAddrStrs[1]; got != want {
t.Fatalf("sc is created with addr %v, want %v", got, want)
@@ -1153,6 +1211,17 @@
//
// Init a(p0) and b(p1); a(p0) is down, use b; move b to p0, a to p1, use b.
func (s) TestPriority_MoveReadyChildToHigherPriority(t *testing.T) {
+ t.Run("caching_enabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, true)
+ testPriorityMoveReadyChildToHigherPriority(t)
+ })
+ t.Run("caching_disabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, false)
+ testPriorityMoveReadyChildToHigherPriority(t)
+ })
+}
+
+func testPriorityMoveReadyChildToHigherPriority(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
@@ -1227,18 +1296,6 @@
t.Fatalf("failed to update ClientConn state: %v", err)
}
- // Old subconn from child-0 should be removed.
- scToShutdown := <-cc.ShutdownSubConnCh
- // The same SubConn is closed by gracefulswitch and pickfirstleaf when they
- // are closed. Remove duplicate events.
- // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
- // workaround once pickfirst is the only leaf policy and responsible for
- // shutting down SubConns.
- <-cc.ShutdownSubConnCh
- if scToShutdown != sc0 {
- t.Fatalf("ShutdownSubConn, want %v, got %v", sc0, scToShutdown)
- }
-
// Because this was a ready child moved to a higher priority, no new subconn
// or picker should be updated.
select {
@@ -1255,6 +1312,17 @@
//
// Init a(p0) and b(p1); a(p0) is down, use b; move b to p0, a to p1, use b.
func (s) TestPriority_RemoveReadyLowestChild(t *testing.T) {
+ t.Run("caching_enabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, true)
+ testPriorityRemoveReadyLowestChild(t)
+ })
+ t.Run("caching_disabled", func(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, false)
+ testPriorityRemoveReadyLowestChild(t)
+ })
+}
+
+func testPriorityRemoveReadyLowestChild(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
@@ -1329,16 +1397,18 @@
t.Fatalf("failed to update ClientConn state: %v", err)
}
- // Old subconn from child-1 should be shut down.
- scToShutdown := <-cc.ShutdownSubConnCh
- // The same SubConn is closed by gracefulswitch and pickfirstleaf when they
- // are closed. Remove duplicate events.
- // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
- // workaround once pickfirst is the only leaf policy and responsible for
- // shutting down SubConns.
- <-cc.ShutdownSubConnCh
- if scToShutdown != sc1 {
- t.Fatalf("ShutdownSubConn, want %v, got %v", sc1, scToShutdown)
+ if !envconfig.EnablePriorityLBChildPolicyCache {
+ // Old subconn from child-1 should be shut down.
+ scToShutdown := <-cc.ShutdownSubConnCh
+ // The same SubConn is closed by gracefulswitch and pickfirstleaf when they
+ // are closed. Remove duplicate events.
+ // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
+ // workaround once pickfirst is the only leaf policy and responsible for
+ // shutting down SubConns.
+ <-cc.ShutdownSubConnCh
+ if scToShutdown != sc1 {
+ t.Fatalf("ShutdownSubConn, want %v, got %v", sc1, scToShutdown)
+ }
}
if err := cc.WaitForErrPicker(ctx); err != nil {
@@ -1358,19 +1428,11 @@
//
// Init 0; 0 is up, use 0; remove 0, only picker is updated, no subconn is
// removed; re-add 0, picker is updated.
-func (s) TestPriority_ReadyChildRemovedButInCache(t *testing.T) {
+func (s) TestPriority_RemoveOnlyReadyChild_CachingEnabled(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, true)
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
defer cancel()
- const testChildCacheTimeout = time.Second
- defer func() func() {
- old := DefaultSubBalancerCloseTimeout
- DefaultSubBalancerCloseTimeout = testChildCacheTimeout
- return func() {
- DefaultSubBalancerCloseTimeout = old
- }
- }()()
-
cc := testutils.NewBalancerClientConn(t)
bb := balancer.Get(Name)
pb := bb.Build(cc, balancer.BuildOptions{})
@@ -1408,8 +1470,7 @@
t.Fatal(err.Error())
}
- // Remove the child, it shouldn't cause any conn changed, but picker should
- // be different.
+ // Remove the child.
if err := pb.UpdateClientConnState(balancer.ClientConnState{
ResolverState: resolver.State{},
BalancerConfig: &LBConfig{},
@@ -1430,7 +1491,7 @@
case <-time.After(time.Millisecond * 100):
}
- // Re-add the child, shouldn't create new connections.
+ // Re-add the child.
if err := pb.UpdateClientConnState(balancer.ClientConnState{
ResolverState: resolver.State{
Endpoints: []resolver.Endpoint{
@@ -1463,6 +1524,109 @@
}
}
+// When the only ready child is removed, it is deleted and subchannels are shut
+// down. Re-adding results in subchannels being recreated.
+//
+// Init 0; 0 is up, use 0; remove 0, re-add 0.
+func (s) TestPriority_RemoveOnlyReadyChild_CachingDisabled(t *testing.T) {
+ testutils.SetEnvConfig(t, &envconfig.EnablePriorityLBChildPolicyCache, false)
+ ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
+ defer cancel()
+
+ cc := testutils.NewBalancerClientConn(t)
+ bb := balancer.Get(Name)
+ pb := bb.Build(cc, balancer.BuildOptions{})
+ defer pb.Close()
+
+ // One children, with priorities [0], with one backend.
+ if err := pb.UpdateClientConnState(balancer.ClientConnState{
+ ResolverState: resolver.State{
+ Endpoints: []resolver.Endpoint{
+ hierarchy.SetInEndpoint(resolver.Endpoint{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[0]}}}, []string{"child-0"}),
+ },
+ },
+ BalancerConfig: &LBConfig{
+ Children: map[string]*Child{
+ "child-0": {Config: &internalserviceconfig.BalancerConfig{Name: roundrobin.Name}},
+ },
+ Priorities: []string{"child-0"},
+ },
+ }); err != nil {
+ t.Fatalf("failed to update ClientConn state: %v", err)
+ }
+
+ addrs1 := <-cc.NewSubConnAddrsCh
+ if got, want := addrs1[0].Addr, testBackendAddrStrs[0]; got != want {
+ t.Fatalf("sc is created with addr %v, want %v", got, want)
+ }
+ sc1 := <-cc.NewSubConnCh
+
+ // p0 is ready.
+ sc1.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
+ sc1.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
+
+ // Test roundrobin with only p0 subconns.
+ if err := cc.WaitForRoundRobinPicker(ctx, sc1); err != nil {
+ t.Fatal(err.Error())
+ }
+
+ // Remove the child.
+ if err := pb.UpdateClientConnState(balancer.ClientConnState{
+ ResolverState: resolver.State{},
+ BalancerConfig: &LBConfig{},
+ }); err != nil {
+ t.Fatalf("failed to update ClientConn state: %v", err)
+ }
+
+ // p0 SubConns are shut down.
+ scToShutdown := <-cc.ShutdownSubConnCh
+ // The same SubConn is closed by gracefulswitch and pickfirstleaf when
+ // they are closed. Remove duplicate events.
+ // TODO: https://github.com/grpc/grpc-go/issues/6472 - Remove this
+ // workaround once pickfirst is the only leaf policy and responsible for
+ // shutting down SubConns.
+ <-cc.ShutdownSubConnCh
+ if scToShutdown != sc1 {
+ t.Fatalf("ShutdownSubConn, want %v, got %v", sc1, scToShutdown)
+ }
+
+ if err := cc.WaitForPickerWithErr(ctx, ErrAllPrioritiesRemoved); err != nil {
+ t.Fatal(err.Error())
+ }
+
+ // Re-add the child.
+ if err := pb.UpdateClientConnState(balancer.ClientConnState{
+ ResolverState: resolver.State{
+ Endpoints: []resolver.Endpoint{
+ hierarchy.SetInEndpoint(resolver.Endpoint{Addresses: []resolver.Address{{Addr: testBackendAddrStrs[0]}}}, []string{"child-0"}),
+ },
+ },
+ BalancerConfig: &LBConfig{
+ Children: map[string]*Child{
+ "child-0": {Config: &internalserviceconfig.BalancerConfig{Name: roundrobin.Name}},
+ },
+ Priorities: []string{"child-0"},
+ },
+ }); err != nil {
+ t.Fatalf("failed to update ClientConn state: %v", err)
+ }
+
+ addrs1 = <-cc.NewSubConnAddrsCh
+ if got, want := addrs1[0].Addr, testBackendAddrStrs[0]; got != want {
+ t.Fatalf("sc is created with addr %v, want %v", got, want)
+ }
+ sc1 = <-cc.NewSubConnCh
+
+ // p0 is ready.
+ sc1.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Connecting})
+ sc1.UpdateState(balancer.SubConnState{ConnectivityState: connectivity.Ready})
+
+ // Test roundrobin with only p0 subconns.
+ if err := cc.WaitForRoundRobinPicker(ctx, sc1); err != nil {
+ t.Fatal(err.Error())
+ }
+}
+
// When the policy of a child is changed.
//
// Init 0; 0 is up, use 0; change 0's policy, 0 is used.
@@ -1691,7 +1855,6 @@
case <-time.After(time.Second):
t.Fatalf("timeout waiting for ResolveNow()")
}
-
}
// TestPriority_IgnoreReresolutionRequestTwoChildren tests the case where the
@@ -1791,7 +1954,7 @@
}
}
-const initIdleBalancerName = "test-init-Idle-balancer"
+const initIdleBalancerName = "test-init-idle-balancer"
var errsTestInitIdle = []error{
fmt.Errorf("init Idle balancer error 0"),