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 }