/*
 *
 * Copyright 2024 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 pickfirstleaf contains the pick_first load balancing policy which // will be the universal leaf policy after dualstack changes are implemented. // // # Experimental // // Notice: This package is EXPERIMENTAL and may be changed or removed in a // later release.
package pickfirstleaf import ( expstats internalgrpclog ) func () { if envconfig.NewPickFirstEnabled { // Register as the default pick_first balancer. Name = "pick_first" } balancer.Register(pickfirstBuilder{}) } // enableHealthListenerKeyType is a unique key type used in resolver // attributes to indicate whether the health listener usage is enabled. type enableHealthListenerKeyType struct{} var ( logger = grpclog.Component("pick-first-leaf-lb") // Name is the name of the pick_first_leaf balancer. // It is changed to "pick_first" in init() if this balancer is to be // registered as the default pickfirst. Name = "pick_first_leaf" disconnectionsMetric = expstats.RegisterInt64Count(expstats.MetricDescriptor{ Name: "grpc.lb.pick_first.disconnections", Description: "EXPERIMENTAL. Number of times the selected subchannel becomes disconnected.", Unit: "{disconnection}", Labels: []string{"grpc.target"}, Default: false, }) connectionAttemptsSucceededMetric = expstats.RegisterInt64Count(expstats.MetricDescriptor{ Name: "grpc.lb.pick_first.connection_attempts_succeeded", Description: "EXPERIMENTAL. Number of successful connection attempts.", Unit: "{attempt}", Labels: []string{"grpc.target"}, Default: false, }) connectionAttemptsFailedMetric = expstats.RegisterInt64Count(expstats.MetricDescriptor{ Name: "grpc.lb.pick_first.connection_attempts_failed", Description: "EXPERIMENTAL. Number of failed connection attempts.", Unit: "{attempt}", Labels: []string{"grpc.target"}, Default: false, }) ) const ( // TODO: change to pick-first when this becomes the default pick_first policy. logPrefix = "[pick-first-leaf-lb %p] " // connectionDelayInterval is the time to wait for during the happy eyeballs // pass before starting the next connection attempt. connectionDelayInterval = 250 * time.Millisecond ) type ipAddrFamily int const ( // ipAddrFamilyUnknown represents strings that can't be parsed as an IP // address. ipAddrFamilyUnknown ipAddrFamily = iota ipAddrFamilyV4 ipAddrFamilyV6 ) type pickfirstBuilder struct{} func (pickfirstBuilder) ( balancer.ClientConn, balancer.BuildOptions) balancer.Balancer { := &pickfirstBalancer{ cc: , target: .Target.String(), metricsRecorder: .MetricsRecorder(), subConns: resolver.NewAddressMapV2[*scData](), state: connectivity.Connecting, cancelConnectionTimer: func() {}, } .logger = internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf(logPrefix, )) return } func ( pickfirstBuilder) () string { return Name } func (pickfirstBuilder) ( json.RawMessage) (serviceconfig.LoadBalancingConfig, error) { var pfConfig if := json.Unmarshal(, &); != nil { return nil, fmt.Errorf("pickfirst: unable to unmarshal LB policy config: %s, error: %v", string(), ) } return , nil } // EnableHealthListener updates the state to configure pickfirst for using a // generic health listener. func ( resolver.State) resolver.State { .Attributes = .Attributes.WithValue(enableHealthListenerKeyType{}, true) return } type pfConfig struct { serviceconfig.LoadBalancingConfig `json:"-"` // If set to true, instructs the LB policy to shuffle the order of the list // of endpoints received from the name resolver before attempting to // connect to them. ShuffleAddressList bool `json:"shuffleAddressList"` } // scData keeps track of the current state of the subConn. // It is not safe for concurrent access. type scData struct { // The following fields are initialized at build time and read-only after // that. subConn balancer.SubConn addr resolver.Address rawConnectivityState connectivity.State // The effective connectivity state based on raw connectivity, health state // and after following sticky TransientFailure behaviour defined in A62. effectiveState connectivity.State lastErr error connectionFailedInFirstPass bool } func ( *pickfirstBalancer) ( resolver.Address) (*scData, error) { := &scData{ rawConnectivityState: connectivity.Idle, effectiveState: connectivity.Idle, addr: , } , := .cc.NewSubConn([]resolver.Address{}, balancer.NewSubConnOptions{ StateListener: func( balancer.SubConnState) { .updateSubConnState(, ) }, }) if != nil { return nil, } .subConn = return , nil } type pickfirstBalancer struct { // The following fields are initialized at build time and read-only after // that and therefore do not need to be guarded by a mutex. logger *internalgrpclog.PrefixLogger cc balancer.ClientConn target string metricsRecorder expstats.MetricsRecorder // guaranteed to be non nil // The mutex is used to ensure synchronization of updates triggered // from the idle picker and the already serialized resolver, // SubConn state updates. mu sync.Mutex // State reported to the channel based on SubConn states and resolver // updates. state connectivity.State // scData for active subonns mapped by address. subConns *resolver.AddressMapV2[*scData] addressList addressList firstPass bool numTF int cancelConnectionTimer func() healthCheckingEnabled bool } // ResolverError is called by the ClientConn when the name resolver produces // an error or when pickfirst determined the resolver update to be invalid. func ( *pickfirstBalancer) ( error) { .mu.Lock() defer .mu.Unlock() .resolverErrorLocked() } func ( *pickfirstBalancer) ( error) { if .logger.V(2) { .logger.Infof("Received error from the name resolver: %v", ) } // The picker will not change since the balancer does not currently // report an error. If the balancer hasn't received a single good resolver // update yet, transition to TRANSIENT_FAILURE. if .state != connectivity.TransientFailure && .addressList.size() > 0 { if .logger.V(2) { .logger.Infof("Ignoring resolver error because balancer is using a previous good update.") } return } .updateBalancerState(balancer.State{ ConnectivityState: connectivity.TransientFailure, Picker: &picker{err: fmt.Errorf("name resolver error: %v", )}, }) } func ( *pickfirstBalancer) ( balancer.ClientConnState) error { .mu.Lock() defer .mu.Unlock() .cancelConnectionTimer() if len(.ResolverState.Addresses) == 0 && len(.ResolverState.Endpoints) == 0 { // Cleanup state pertaining to the previous resolver state. // Treat an empty address list like an error by calling b.ResolverError. .closeSubConnsLocked() .addressList.updateAddrs(nil) .resolverErrorLocked(errors.New("produced zero addresses")) return balancer.ErrBadResolverState } .healthCheckingEnabled = .ResolverState.Attributes.Value(enableHealthListenerKeyType{}) != nil , := .BalancerConfig.(pfConfig) if .BalancerConfig != nil && ! { return fmt.Errorf("pickfirst: received illegal BalancerConfig (type %T): %v: %w", .BalancerConfig, .BalancerConfig, balancer.ErrBadResolverState) } if .logger.V(2) { .logger.Infof("Received new config %s, resolver state %s", pretty.ToJSON(), pretty.ToJSON(.ResolverState)) } var []resolver.Address if := .ResolverState.Endpoints; len() != 0 { // Perform the optional shuffling described in gRFC A62. The shuffling // will change the order of endpoints but not touch the order of the // addresses within each endpoint. - A61 if .ShuffleAddressList { = append([]resolver.Endpoint{}, ...) internal.RandShuffle(len(), func(, int) { [], [] = [], [] }) } // "Flatten the list by concatenating the ordered list of addresses for // each of the endpoints, in order." - A61 for , := range { = append(, .Addresses...) } } else { // Endpoints not set, process addresses until we migrate resolver // emissions fully to Endpoints. The top channel does wrap emitted // addresses with endpoints, however some balancers such as weighted // target do not forward the corresponding correct endpoints down/split // endpoints properly. Once all balancers correctly forward endpoints // down, can delete this else conditional. = .ResolverState.Addresses if .ShuffleAddressList { = append([]resolver.Address{}, ...) internal.RandShuffle(len(), func(, int) { [], [] = [], [] }) } } // If an address appears in multiple endpoints or in the same endpoint // multiple times, we keep it only once. We will create only one SubConn // for the address because an AddressMap is used to store SubConns. // Not de-duplicating would result in attempting to connect to the same // SubConn multiple times in the same pass. We don't want this. = deDupAddresses() = interleaveAddresses() := .addressList.currentAddress() , := .subConns.Get() := .addressList.size() := && .rawConnectivityState == connectivity.Ready .addressList.updateAddrs() // If the previous ready SubConn exists in new address list, // keep this connection and don't create new SubConns. if && .addressList.seekTo() { return nil } .reconcileSubConnsLocked() // If it's the first resolver update or the balancer was already READY // (but the new address list does not contain the ready SubConn) or // CONNECTING, enter CONNECTING. // We may be in TRANSIENT_FAILURE due to a previous empty address list, // we should still enter CONNECTING because the sticky TF behaviour // mentioned in A62 applies only when the TRANSIENT_FAILURE is reported // due to connectivity failures. if || .state == connectivity.Connecting || == 0 { // Start connection attempt at first address. .forceUpdateConcludedStateLocked(balancer.State{ ConnectivityState: connectivity.Connecting, Picker: &picker{err: balancer.ErrNoSubConnAvailable}, }) .startFirstPassLocked() } else if .state == connectivity.TransientFailure { // If we're in TRANSIENT_FAILURE, we stay in TRANSIENT_FAILURE until // we're READY. See A62. .startFirstPassLocked() } return nil } // UpdateSubConnState is unused as a StateListener is always registered when // creating SubConns. func ( *pickfirstBalancer) ( balancer.SubConn, balancer.SubConnState) { .logger.Errorf("UpdateSubConnState(%v, %+v) called unexpectedly", , ) } func ( *pickfirstBalancer) () { .mu.Lock() defer .mu.Unlock() .closeSubConnsLocked() .cancelConnectionTimer() .state = connectivity.Shutdown } // ExitIdle moves the balancer out of idle state. It can be called concurrently // by the idlePicker and clientConn so access to variables should be // synchronized. func ( *pickfirstBalancer) () { .mu.Lock() defer .mu.Unlock() if .state == connectivity.Idle { .startFirstPassLocked() } } func ( *pickfirstBalancer) () { .firstPass = true .numTF = 0 // Reset the connection attempt record for existing SubConns. for , := range .subConns.Values() { .connectionFailedInFirstPass = false } .requestConnectionLocked() } func ( *pickfirstBalancer) () { for , := range .subConns.Values() { .subConn.Shutdown() } .subConns = resolver.NewAddressMapV2[*scData]() } // deDupAddresses ensures that each address appears only once in the slice. func ( []resolver.Address) []resolver.Address { := resolver.NewAddressMapV2[*scData]() := []resolver.Address{} for , := range { if , := .Get(); { continue } = append(, ) } return } // interleaveAddresses interleaves addresses of both families (IPv4 and IPv6) // as per RFC-8305 section 4. // Whichever address family is first in the list is followed by an address of // the other address family; that is, if the first address in the list is IPv6, // then the first IPv4 address should be moved up in the list to be second in // the list. It doesn't support configuring "First Address Family Count", i.e. // there will always be a single member of the first address family at the // beginning of the interleaved list. // Addresses that are neither IPv4 nor IPv6 are treated as part of a third // "unknown" family for interleaving. // See: https://datatracker.ietf.org/doc/html/rfc8305#autoid-6 func ( []resolver.Address) []resolver.Address { := map[ipAddrFamily][]resolver.Address{} := []ipAddrFamily{} for , := range { := addressFamily(.Addr) if , := []; ! { = append(, ) } [] = append([], ) } := make([]resolver.Address, 0, len()) for := 0; len() < len(); = ( + 1) % len() { // Some IP types may have fewer addresses than others, so we look for // the next type that has a remaining member to add to the interleaved // list. := [] := [] if len() > 0 { = append(, [0]) [] = [1:] } } return } // addressFamily returns the ipAddrFamily after parsing the address string. // If the address isn't of the format "ip-address:port", it returns // ipAddrFamilyUnknown. The address may be valid even if it's not an IP when // using a resolver like passthrough where the address may be a hostname in // some format that the dialer can resolve. func ( string) ipAddrFamily { // Parse the IP after removing the port. , , := net.SplitHostPort() if != nil { return ipAddrFamilyUnknown } , := netip.ParseAddr() if != nil { return ipAddrFamilyUnknown } switch { case .Is4() || .Is4In6(): return ipAddrFamilyV4 case .Is6(): return ipAddrFamilyV6 default: return ipAddrFamilyUnknown } } // reconcileSubConnsLocked updates the active subchannels based on a new address // list from the resolver. It does this by: // - closing subchannels: any existing subchannels associated with addresses // that are no longer in the updated list are shut down. // - removing subchannels: entries for these closed subchannels are removed // from the subchannel map. // // This ensures that the subchannel map accurately reflects the current set of // addresses received from the name resolver. func ( *pickfirstBalancer) ( []resolver.Address) { := resolver.NewAddressMapV2[bool]() for , := range { .Set(, true) } for , := range .subConns.Keys() { if , := .Get(); { continue } , := .subConns.Get() .subConn.Shutdown() .subConns.Delete() } } // shutdownRemainingLocked shuts down remaining subConns. Called when a subConn // becomes ready, which means that all other subConn must be shutdown. func ( *pickfirstBalancer) ( *scData) { .cancelConnectionTimer() for , := range .subConns.Values() { if .subConn != .subConn { .subConn.Shutdown() } } .subConns = resolver.NewAddressMapV2[*scData]() .subConns.Set(.addr, ) } // requestConnectionLocked starts connecting on the subchannel corresponding to // the current address. If no subchannel exists, one is created. If the current // subchannel is in TransientFailure, a connection to the next address is // attempted until a subchannel is found. func ( *pickfirstBalancer) () { if !.addressList.isValid() { return } var error for := true; ; = .addressList.increment() { := .addressList.currentAddress() , := .subConns.Get() if ! { var error // We want to assign the new scData to sd from the outer scope, // hence we can't use := below. , = .newSCData() if != nil { // This should never happen, unless the clientConn is being shut // down. if .logger.V(2) { .logger.Infof("Failed to create a subConn for address %v: %v", .String(), ) } // Do nothing, the LB policy will be closed soon. return } .subConns.Set(, ) } switch .rawConnectivityState { case connectivity.Idle: .subConn.Connect() .scheduleNextConnectionLocked() return case connectivity.TransientFailure: // The SubConn is being re-used and failed during a previous pass // over the addressList. It has not completed backoff yet. // Mark it as having failed and try the next address. .connectionFailedInFirstPass = true = .lastErr continue case connectivity.Connecting: // Wait for the connection attempt to complete or the timer to fire // before attempting the next address. .scheduleNextConnectionLocked() return default: .logger.Errorf("SubConn with unexpected state %v present in SubConns map.", .rawConnectivityState) return } } // All the remaining addresses in the list are in TRANSIENT_FAILURE, end the // first pass if possible. .endFirstPassIfPossibleLocked() } func ( *pickfirstBalancer) () { .cancelConnectionTimer() if !.addressList.hasNext() { return } := .addressList.currentAddress() := false // Access to this is protected by the balancer's mutex. := internal.TimeAfterFunc(connectionDelayInterval, func() { .mu.Lock() defer .mu.Unlock() // If the scheduled task is cancelled while acquiring the mutex, return. if { return } if .logger.V(2) { .logger.Infof("Happy Eyeballs timer expired while waiting for connection to %q.", .Addr) } if .addressList.increment() { .requestConnectionLocked() } }) // Access to the cancellation callback held by the balancer is guarded by // the balancer's mutex, so it's safe to set the boolean from the callback. .cancelConnectionTimer = sync.OnceFunc(func() { = true () }) } func ( *pickfirstBalancer) ( *scData, balancer.SubConnState) { .mu.Lock() defer .mu.Unlock() := .rawConnectivityState .rawConnectivityState = .ConnectivityState // Previously relevant SubConns can still callback with state updates. // To prevent pickers from returning these obsolete SubConns, this logic // is included to check if the current list of active SubConns includes this // SubConn. if !.isActiveSCData() { return } if .ConnectivityState == connectivity.Shutdown { .effectiveState = connectivity.Shutdown return } // Record a connection attempt when exiting CONNECTING. if .ConnectivityState == connectivity.TransientFailure { .connectionFailedInFirstPass = true connectionAttemptsFailedMetric.Record(.metricsRecorder, 1, .target) } if .ConnectivityState == connectivity.Ready { connectionAttemptsSucceededMetric.Record(.metricsRecorder, 1, .target) .shutdownRemainingLocked() if !.addressList.seekTo(.addr) { // This should not fail as we should have only one SubConn after // entering READY. The SubConn should be present in the addressList. .logger.Errorf("Address %q not found address list in %v", .addr, .addressList.addresses) return } if !.healthCheckingEnabled { if .logger.V(2) { .logger.Infof("SubConn %p reported connectivity state READY and the health listener is disabled. Transitioning SubConn to READY.", .subConn) } .effectiveState = connectivity.Ready .updateBalancerState(balancer.State{ ConnectivityState: connectivity.Ready, Picker: &picker{result: balancer.PickResult{SubConn: .subConn}}, }) return } if .logger.V(2) { .logger.Infof("SubConn %p reported connectivity state READY. Registering health listener.", .subConn) } // Send a CONNECTING update to take the SubConn out of sticky-TF if // required. .effectiveState = connectivity.Connecting .updateBalancerState(balancer.State{ ConnectivityState: connectivity.Connecting, Picker: &picker{err: balancer.ErrNoSubConnAvailable}, }) .subConn.RegisterHealthListener(func( balancer.SubConnState) { .updateSubConnHealthState(, ) }) return } // If the LB policy is READY, and it receives a subchannel state change, // it means that the READY subchannel has failed. // A SubConn can also transition from CONNECTING directly to IDLE when // a transport is successfully created, but the connection fails // before the SubConn can send the notification for READY. We treat // this as a successful connection and transition to IDLE. // TODO: https://github.com/grpc/grpc-go/issues/7862 - Remove the second // part of the if condition below once the issue is fixed. if == connectivity.Ready || ( == connectivity.Connecting && .ConnectivityState == connectivity.Idle) { // Once a transport fails, the balancer enters IDLE and starts from // the first address when the picker is used. .shutdownRemainingLocked() .effectiveState = .ConnectivityState // READY SubConn interspliced in between CONNECTING and IDLE, need to // account for that. if == connectivity.Connecting { // A known issue (https://github.com/grpc/grpc-go/issues/7862) // causes a race that prevents the READY state change notification. // This works around it. connectionAttemptsSucceededMetric.Record(.metricsRecorder, 1, .target) } disconnectionsMetric.Record(.metricsRecorder, 1, .target) .addressList.reset() .updateBalancerState(balancer.State{ ConnectivityState: connectivity.Idle, Picker: &idlePicker{exitIdle: sync.OnceFunc(.ExitIdle)}, }) return } if .firstPass { switch .ConnectivityState { case connectivity.Connecting: // The effective state can be in either IDLE, CONNECTING or // TRANSIENT_FAILURE. If it's TRANSIENT_FAILURE, stay in // TRANSIENT_FAILURE until it's READY. See A62. if .effectiveState != connectivity.TransientFailure { .effectiveState = connectivity.Connecting .updateBalancerState(balancer.State{ ConnectivityState: connectivity.Connecting, Picker: &picker{err: balancer.ErrNoSubConnAvailable}, }) } case connectivity.TransientFailure: .lastErr = .ConnectionError .effectiveState = connectivity.TransientFailure // Since we're re-using common SubConns while handling resolver // updates, we could receive an out of turn TRANSIENT_FAILURE from // a pass over the previous address list. Happy Eyeballs will also // cause out of order updates to arrive. if := .addressList.currentAddress(); equalAddressIgnoringBalAttributes(&, &.addr) { .cancelConnectionTimer() if .addressList.increment() { .requestConnectionLocked() return } } // End the first pass if we've seen a TRANSIENT_FAILURE from all // SubConns once. .endFirstPassIfPossibleLocked(.ConnectionError) } return } // We have finished the first pass, keep re-connecting failing SubConns. switch .ConnectivityState { case connectivity.TransientFailure: .numTF = (.numTF + 1) % .subConns.Len() .lastErr = .ConnectionError if .numTF%.subConns.Len() == 0 { .updateBalancerState(balancer.State{ ConnectivityState: connectivity.TransientFailure, Picker: &picker{err: .ConnectionError}, }) } // We don't need to request re-resolution since the SubConn already // does that before reporting TRANSIENT_FAILURE. // TODO: #7534 - Move re-resolution requests from SubConn into // pick_first. case connectivity.Idle: .subConn.Connect() } } // endFirstPassIfPossibleLocked ends the first happy-eyeballs pass if all the // addresses are tried and their SubConns have reported a failure. func ( *pickfirstBalancer) ( error) { // An optimization to avoid iterating over the entire SubConn map. if .addressList.isValid() { return } // Connect() has been called on all the SubConns. The first pass can be // ended if all the SubConns have reported a failure. for , := range .subConns.Values() { if !.connectionFailedInFirstPass { return } } .firstPass = false .updateBalancerState(balancer.State{ ConnectivityState: connectivity.TransientFailure, Picker: &picker{err: }, }) // Start re-connecting all the SubConns that are already in IDLE. for , := range .subConns.Values() { if .rawConnectivityState == connectivity.Idle { .subConn.Connect() } } } func ( *pickfirstBalancer) ( *scData) bool { , := .subConns.Get(.addr) return && == } func ( *pickfirstBalancer) ( *scData, balancer.SubConnState) { .mu.Lock() defer .mu.Unlock() // Previously relevant SubConns can still callback with state updates. // To prevent pickers from returning these obsolete SubConns, this logic // is included to check if the current list of active SubConns includes // this SubConn. if !.isActiveSCData() { return } .effectiveState = .ConnectivityState switch .ConnectivityState { case connectivity.Ready: .updateBalancerState(balancer.State{ ConnectivityState: connectivity.Ready, Picker: &picker{result: balancer.PickResult{SubConn: .subConn}}, }) case connectivity.TransientFailure: .updateBalancerState(balancer.State{ ConnectivityState: connectivity.TransientFailure, Picker: &picker{err: fmt.Errorf("pickfirst: health check failure: %v", .ConnectionError)}, }) case connectivity.Connecting: .updateBalancerState(balancer.State{ ConnectivityState: connectivity.Connecting, Picker: &picker{err: balancer.ErrNoSubConnAvailable}, }) default: .logger.Errorf("Got unexpected health update for SubConn %p: %v", ) } } // updateBalancerState stores the state reported to the channel and calls // ClientConn.UpdateState(). As an optimization, it avoids sending duplicate // updates to the channel. func ( *pickfirstBalancer) ( balancer.State) { // In case of TransientFailures allow the picker to be updated to update // the connectivity error, in all other cases don't send duplicate state // updates. if .ConnectivityState == .state && .state != connectivity.TransientFailure { return } .forceUpdateConcludedStateLocked() } // forceUpdateConcludedStateLocked stores the state reported to the channel and // calls ClientConn.UpdateState(). // A separate function is defined to force update the ClientConn state since the // channel doesn't correctly assume that LB policies start in CONNECTING and // relies on LB policy to send an initial CONNECTING update. func ( *pickfirstBalancer) ( balancer.State) { .state = .ConnectivityState .cc.UpdateState() } type picker struct { result balancer.PickResult err error } func ( *picker) (balancer.PickInfo) (balancer.PickResult, error) { return .result, .err } // idlePicker is used when the SubConn is IDLE and kicks the SubConn into // CONNECTING when Pick is called. type idlePicker struct { exitIdle func() } func ( *idlePicker) (balancer.PickInfo) (balancer.PickResult, error) { .exitIdle() return balancer.PickResult{}, balancer.ErrNoSubConnAvailable } // addressList manages sequentially iterating over addresses present in a list // of endpoints. It provides a 1 dimensional view of the addresses present in // the endpoints. // This type is not safe for concurrent access. type addressList struct { addresses []resolver.Address idx int } func ( *addressList) () bool { return .idx < len(.addresses) } func ( *addressList) () int { return len(.addresses) } // increment moves to the next index in the address list. // This method returns false if it went off the list, true otherwise. func ( *addressList) () bool { if !.isValid() { return false } .idx++ return .idx < len(.addresses) } // currentAddress returns the current address pointed to in the addressList. // If the list is in an invalid state, it returns an empty address instead. func ( *addressList) () resolver.Address { if !.isValid() { return resolver.Address{} } return .addresses[.idx] } func ( *addressList) () { .idx = 0 } func ( *addressList) ( []resolver.Address) { .addresses = .reset() } // seekTo returns false if the needle was not found and the current index was // left unchanged. func ( *addressList) ( resolver.Address) bool { for , := range .addresses { if !equalAddressIgnoringBalAttributes(&, &) { continue } .idx = return true } return false } // hasNext returns whether incrementing the addressList will result in moving // past the end of the list. If the list has already moved past the end, it // returns false. func ( *addressList) () bool { if !.isValid() { return false } return .idx+1 < len(.addresses) } // equalAddressIgnoringBalAttributes returns true is a and b are considered // equal. This is different from the Equal method on the resolver.Address type // which considers all fields to determine equality. Here, we only consider // fields that are meaningful to the SubConn. func (, *resolver.Address) bool { return .Addr == .Addr && .ServerName == .ServerName && .Attributes.Equal(.Attributes) }