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