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 }