google.golang.org/grpc@v1.72.2/balancer/endpointsharding/endpointsharding.go (about)

     1  /*
     2   *
     3   * Copyright 2024 gRPC authors.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   *
    17   */
    18  
    19  // Package endpointsharding implements a load balancing policy that manages
    20  // homogeneous child policies each owning a single endpoint.
    21  //
    22  // # Experimental
    23  //
    24  // Notice: This package is EXPERIMENTAL and may be changed or removed in a
    25  // later release.
    26  package endpointsharding
    27  
    28  import (
    29  	"errors"
    30  	rand "math/rand/v2"
    31  	"sync"
    32  	"sync/atomic"
    33  
    34  	"google.golang.org/grpc/balancer"
    35  	"google.golang.org/grpc/balancer/base"
    36  	"google.golang.org/grpc/connectivity"
    37  	"google.golang.org/grpc/resolver"
    38  )
    39  
    40  // ChildState is the balancer state of a child along with the endpoint which
    41  // identifies the child balancer.
    42  type ChildState struct {
    43  	Endpoint resolver.Endpoint
    44  	State    balancer.State
    45  
    46  	// Balancer exposes only the ExitIdler interface of the child LB policy.
    47  	// Other methods of the child policy are called only by endpointsharding.
    48  	Balancer balancer.ExitIdler
    49  }
    50  
    51  // Options are the options to configure the behaviour of the
    52  // endpointsharding balancer.
    53  type Options struct {
    54  	// DisableAutoReconnect allows the balancer to keep child balancer in the
    55  	// IDLE state until they are explicitly triggered to exit using the
    56  	// ChildState obtained from the endpointsharding picker. When set to false,
    57  	// the endpointsharding balancer will automatically call ExitIdle on child
    58  	// connections that report IDLE.
    59  	DisableAutoReconnect bool
    60  }
    61  
    62  // ChildBuilderFunc creates a new balancer with the ClientConn. It has the same
    63  // type as the balancer.Builder.Build method.
    64  type ChildBuilderFunc func(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer
    65  
    66  // NewBalancer returns a load balancing policy that manages homogeneous child
    67  // policies each owning a single endpoint. The endpointsharding balancer
    68  // forwards the LoadBalancingConfig in ClientConn state updates to its children.
    69  func NewBalancer(cc balancer.ClientConn, opts balancer.BuildOptions, childBuilder ChildBuilderFunc, esOpts Options) balancer.Balancer {
    70  	es := &endpointSharding{
    71  		cc:           cc,
    72  		bOpts:        opts,
    73  		esOpts:       esOpts,
    74  		childBuilder: childBuilder,
    75  	}
    76  	es.children.Store(resolver.NewEndpointMap[*balancerWrapper]())
    77  	return es
    78  }
    79  
    80  // endpointSharding is a balancer that wraps child balancers. It creates a child
    81  // balancer with child config for every unique Endpoint received. It updates the
    82  // child states on any update from parent or child.
    83  type endpointSharding struct {
    84  	cc           balancer.ClientConn
    85  	bOpts        balancer.BuildOptions
    86  	esOpts       Options
    87  	childBuilder ChildBuilderFunc
    88  
    89  	// childMu synchronizes calls to any single child. It must be held for all
    90  	// calls into a child. To avoid deadlocks, do not acquire childMu while
    91  	// holding mu.
    92  	childMu  sync.Mutex
    93  	children atomic.Pointer[resolver.EndpointMap[*balancerWrapper]]
    94  
    95  	// inhibitChildUpdates is set during UpdateClientConnState/ResolverError
    96  	// calls (calls to children will each produce an update, only want one
    97  	// update).
    98  	inhibitChildUpdates atomic.Bool
    99  
   100  	// mu synchronizes access to the state stored in balancerWrappers in the
   101  	// children field. mu must not be held during calls into a child since
   102  	// synchronous calls back from the child may require taking mu, causing a
   103  	// deadlock. To avoid deadlocks, do not acquire childMu while holding mu.
   104  	mu sync.Mutex
   105  }
   106  
   107  // UpdateClientConnState creates a child for new endpoints and deletes children
   108  // for endpoints that are no longer present. It also updates all the children,
   109  // and sends a single synchronous update of the childrens' aggregated state at
   110  // the end of the UpdateClientConnState operation. If any endpoint has no
   111  // addresses it will ignore that endpoint. Otherwise, returns first error found
   112  // from a child, but fully processes the new update.
   113  func (es *endpointSharding) UpdateClientConnState(state balancer.ClientConnState) error {
   114  	es.childMu.Lock()
   115  	defer es.childMu.Unlock()
   116  
   117  	es.inhibitChildUpdates.Store(true)
   118  	defer func() {
   119  		es.inhibitChildUpdates.Store(false)
   120  		es.updateState()
   121  	}()
   122  	var ret error
   123  
   124  	children := es.children.Load()
   125  	newChildren := resolver.NewEndpointMap[*balancerWrapper]()
   126  
   127  	// Update/Create new children.
   128  	for _, endpoint := range state.ResolverState.Endpoints {
   129  		if _, ok := newChildren.Get(endpoint); ok {
   130  			// Endpoint child was already created, continue to avoid duplicate
   131  			// update.
   132  			continue
   133  		}
   134  		childBalancer, ok := children.Get(endpoint)
   135  		if ok {
   136  			// Endpoint attributes may have changed, update the stored endpoint.
   137  			es.mu.Lock()
   138  			childBalancer.childState.Endpoint = endpoint
   139  			es.mu.Unlock()
   140  		} else {
   141  			childBalancer = &balancerWrapper{
   142  				childState: ChildState{Endpoint: endpoint},
   143  				ClientConn: es.cc,
   144  				es:         es,
   145  			}
   146  			childBalancer.childState.Balancer = childBalancer
   147  			childBalancer.child = es.childBuilder(childBalancer, es.bOpts)
   148  		}
   149  		newChildren.Set(endpoint, childBalancer)
   150  		if err := childBalancer.updateClientConnStateLocked(balancer.ClientConnState{
   151  			BalancerConfig: state.BalancerConfig,
   152  			ResolverState: resolver.State{
   153  				Endpoints:  []resolver.Endpoint{endpoint},
   154  				Attributes: state.ResolverState.Attributes,
   155  			},
   156  		}); err != nil && ret == nil {
   157  			// Return first error found, and always commit full processing of
   158  			// updating children. If desired to process more specific errors
   159  			// across all endpoints, caller should make these specific
   160  			// validations, this is a current limitation for simplicity sake.
   161  			ret = err
   162  		}
   163  	}
   164  	// Delete old children that are no longer present.
   165  	for _, e := range children.Keys() {
   166  		child, _ := children.Get(e)
   167  		if _, ok := newChildren.Get(e); !ok {
   168  			child.closeLocked()
   169  		}
   170  	}
   171  	es.children.Store(newChildren)
   172  	if newChildren.Len() == 0 {
   173  		return balancer.ErrBadResolverState
   174  	}
   175  	return ret
   176  }
   177  
   178  // ResolverError forwards the resolver error to all of the endpointSharding's
   179  // children and sends a single synchronous update of the childStates at the end
   180  // of the ResolverError operation.
   181  func (es *endpointSharding) ResolverError(err error) {
   182  	es.childMu.Lock()
   183  	defer es.childMu.Unlock()
   184  	es.inhibitChildUpdates.Store(true)
   185  	defer func() {
   186  		es.inhibitChildUpdates.Store(false)
   187  		es.updateState()
   188  	}()
   189  	children := es.children.Load()
   190  	for _, child := range children.Values() {
   191  		child.resolverErrorLocked(err)
   192  	}
   193  }
   194  
   195  func (es *endpointSharding) UpdateSubConnState(balancer.SubConn, balancer.SubConnState) {
   196  	// UpdateSubConnState is deprecated.
   197  }
   198  
   199  func (es *endpointSharding) Close() {
   200  	es.childMu.Lock()
   201  	defer es.childMu.Unlock()
   202  	children := es.children.Load()
   203  	for _, child := range children.Values() {
   204  		child.closeLocked()
   205  	}
   206  }
   207  
   208  // updateState updates this component's state. It sends the aggregated state,
   209  // and a picker with round robin behavior with all the child states present if
   210  // needed.
   211  func (es *endpointSharding) updateState() {
   212  	if es.inhibitChildUpdates.Load() {
   213  		return
   214  	}
   215  	var readyPickers, connectingPickers, idlePickers, transientFailurePickers []balancer.Picker
   216  
   217  	es.mu.Lock()
   218  	defer es.mu.Unlock()
   219  
   220  	children := es.children.Load()
   221  	childStates := make([]ChildState, 0, children.Len())
   222  
   223  	for _, child := range children.Values() {
   224  		childState := child.childState
   225  		childStates = append(childStates, childState)
   226  		childPicker := childState.State.Picker
   227  		switch childState.State.ConnectivityState {
   228  		case connectivity.Ready:
   229  			readyPickers = append(readyPickers, childPicker)
   230  		case connectivity.Connecting:
   231  			connectingPickers = append(connectingPickers, childPicker)
   232  		case connectivity.Idle:
   233  			idlePickers = append(idlePickers, childPicker)
   234  		case connectivity.TransientFailure:
   235  			transientFailurePickers = append(transientFailurePickers, childPicker)
   236  			// connectivity.Shutdown shouldn't appear.
   237  		}
   238  	}
   239  
   240  	// Construct the round robin picker based off the aggregated state. Whatever
   241  	// the aggregated state, use the pickers present that are currently in that
   242  	// state only.
   243  	var aggState connectivity.State
   244  	var pickers []balancer.Picker
   245  	if len(readyPickers) >= 1 {
   246  		aggState = connectivity.Ready
   247  		pickers = readyPickers
   248  	} else if len(connectingPickers) >= 1 {
   249  		aggState = connectivity.Connecting
   250  		pickers = connectingPickers
   251  	} else if len(idlePickers) >= 1 {
   252  		aggState = connectivity.Idle
   253  		pickers = idlePickers
   254  	} else if len(transientFailurePickers) >= 1 {
   255  		aggState = connectivity.TransientFailure
   256  		pickers = transientFailurePickers
   257  	} else {
   258  		aggState = connectivity.TransientFailure
   259  		pickers = []balancer.Picker{base.NewErrPicker(errors.New("no children to pick from"))}
   260  	} // No children (resolver error before valid update).
   261  	p := &pickerWithChildStates{
   262  		pickers:     pickers,
   263  		childStates: childStates,
   264  		next:        uint32(rand.IntN(len(pickers))),
   265  	}
   266  	es.cc.UpdateState(balancer.State{
   267  		ConnectivityState: aggState,
   268  		Picker:            p,
   269  	})
   270  }
   271  
   272  // pickerWithChildStates delegates to the pickers it holds in a round robin
   273  // fashion. It also contains the childStates of all the endpointSharding's
   274  // children.
   275  type pickerWithChildStates struct {
   276  	pickers     []balancer.Picker
   277  	childStates []ChildState
   278  	next        uint32
   279  }
   280  
   281  func (p *pickerWithChildStates) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
   282  	nextIndex := atomic.AddUint32(&p.next, 1)
   283  	picker := p.pickers[nextIndex%uint32(len(p.pickers))]
   284  	return picker.Pick(info)
   285  }
   286  
   287  // ChildStatesFromPicker returns the state of all the children managed by the
   288  // endpoint sharding balancer that created this picker.
   289  func ChildStatesFromPicker(picker balancer.Picker) []ChildState {
   290  	p, ok := picker.(*pickerWithChildStates)
   291  	if !ok {
   292  		return nil
   293  	}
   294  	return p.childStates
   295  }
   296  
   297  // balancerWrapper is a wrapper of a balancer. It ID's a child balancer by
   298  // endpoint, and persists recent child balancer state.
   299  type balancerWrapper struct {
   300  	// The following fields are initialized at build time and read-only after
   301  	// that and therefore do not need to be guarded by a mutex.
   302  
   303  	// child contains the wrapped balancer. Access its methods only through
   304  	// methods on balancerWrapper to ensure proper synchronization
   305  	child               balancer.Balancer
   306  	balancer.ClientConn // embed to intercept UpdateState, doesn't deal with SubConns
   307  
   308  	es *endpointSharding
   309  
   310  	// Access to the following fields is guarded by es.mu.
   311  
   312  	childState ChildState
   313  	isClosed   bool
   314  }
   315  
   316  func (bw *balancerWrapper) UpdateState(state balancer.State) {
   317  	bw.es.mu.Lock()
   318  	bw.childState.State = state
   319  	bw.es.mu.Unlock()
   320  	if state.ConnectivityState == connectivity.Idle && !bw.es.esOpts.DisableAutoReconnect {
   321  		bw.ExitIdle()
   322  	}
   323  	bw.es.updateState()
   324  }
   325  
   326  // ExitIdle pings an IDLE child balancer to exit idle in a new goroutine to
   327  // avoid deadlocks due to synchronous balancer state updates.
   328  func (bw *balancerWrapper) ExitIdle() {
   329  	if ei, ok := bw.child.(balancer.ExitIdler); ok {
   330  		go func() {
   331  			bw.es.childMu.Lock()
   332  			if !bw.isClosed {
   333  				ei.ExitIdle()
   334  			}
   335  			bw.es.childMu.Unlock()
   336  		}()
   337  	}
   338  }
   339  
   340  // updateClientConnStateLocked delivers the ClientConnState to the child
   341  // balancer. Callers must hold the child mutex of the parent endpointsharding
   342  // balancer.
   343  func (bw *balancerWrapper) updateClientConnStateLocked(ccs balancer.ClientConnState) error {
   344  	return bw.child.UpdateClientConnState(ccs)
   345  }
   346  
   347  // closeLocked closes the child balancer. Callers must hold the child mutext of
   348  // the parent endpointsharding balancer.
   349  func (bw *balancerWrapper) closeLocked() {
   350  	bw.child.Close()
   351  	bw.isClosed = true
   352  }
   353  
   354  func (bw *balancerWrapper) resolverErrorLocked(err error) {
   355  	bw.child.ResolverError(err)
   356  }