| /* |
| * |
| * Copyright 2025 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 lazy contains a load balancer that starts in IDLE instead of |
| // CONNECTING. Once it starts connecting, it instantiates its delegate. |
| // |
| // # Experimental |
| // |
| // Notice: This package is EXPERIMENTAL and may be changed or removed in a |
| // later release. |
| package lazy |
| |
| import ( |
| "fmt" |
| "sync" |
| |
| "google.golang.org/grpc/balancer" |
| "google.golang.org/grpc/connectivity" |
| "google.golang.org/grpc/grpclog" |
| "google.golang.org/grpc/resolver" |
| |
| internalgrpclog "google.golang.org/grpc/internal/grpclog" |
| ) |
| |
| var ( |
| logger = grpclog.Component("lazy-lb") |
| ) |
| |
| const ( |
| logPrefix = "[lazy-lb %p] " |
| ) |
| |
| // ChildBuilderFunc creates a new balancer with the ClientConn. It has the same |
| // type as the balancer.Builder.Build method. |
| type ChildBuilderFunc func(cc balancer.ClientConn, opts balancer.BuildOptions) balancer.Balancer |
| |
| // NewBalancer is the constructor for the lazy balancer. |
| func NewBalancer(cc balancer.ClientConn, bOpts balancer.BuildOptions, childBuilder ChildBuilderFunc) balancer.Balancer { |
| b := &lazyBalancer{ |
| cc: cc, |
| buildOptions: bOpts, |
| childBuilder: childBuilder, |
| } |
| b.logger = internalgrpclog.NewPrefixLogger(logger, fmt.Sprintf(logPrefix, b)) |
| cc.UpdateState(balancer.State{ |
| ConnectivityState: connectivity.Idle, |
| Picker: &idlePicker{exitIdle: sync.OnceFunc(func() { |
| // Call ExitIdle in a new goroutine to avoid deadlocks while calling |
| // back into the channel synchronously. |
| go b.ExitIdle() |
| })}, |
| }) |
| return b |
| } |
| |
| type lazyBalancer 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. |
| cc balancer.ClientConn |
| buildOptions balancer.BuildOptions |
| logger *internalgrpclog.PrefixLogger |
| childBuilder ChildBuilderFunc |
| |
| // The following fields are accessed while handling calls to the idlePicker |
| // and when handling ClientConn state updates. They are guarded by a mutex. |
| |
| mu sync.Mutex |
| delegate balancer.Balancer |
| latestClientConnState *balancer.ClientConnState |
| latestResolverError error |
| } |
| |
| func (lb *lazyBalancer) Close() { |
| lb.mu.Lock() |
| defer lb.mu.Unlock() |
| if lb.delegate != nil { |
| lb.delegate.Close() |
| lb.delegate = nil |
| } |
| } |
| |
| func (lb *lazyBalancer) ResolverError(err error) { |
| lb.mu.Lock() |
| defer lb.mu.Unlock() |
| if lb.delegate != nil { |
| lb.delegate.ResolverError(err) |
| return |
| } |
| lb.latestResolverError = err |
| } |
| |
| func (lb *lazyBalancer) UpdateClientConnState(ccs balancer.ClientConnState) error { |
| lb.mu.Lock() |
| defer lb.mu.Unlock() |
| if lb.delegate != nil { |
| return lb.delegate.UpdateClientConnState(ccs) |
| } |
| |
| lb.latestClientConnState = &ccs |
| lb.latestResolverError = nil |
| return nil |
| } |
| |
| // UpdateSubConnState implements balancer.Balancer. |
| func (lb *lazyBalancer) UpdateSubConnState(balancer.SubConn, balancer.SubConnState) { |
| // UpdateSubConnState is deprecated. |
| } |
| |
| func (lb *lazyBalancer) ExitIdle() { |
| lb.mu.Lock() |
| defer lb.mu.Unlock() |
| if lb.delegate != nil { |
| lb.delegate.ExitIdle() |
| return |
| } |
| lb.delegate = lb.childBuilder(lb.cc, lb.buildOptions) |
| if lb.latestClientConnState != nil { |
| if err := lb.delegate.UpdateClientConnState(*lb.latestClientConnState); err != nil { |
| if err == balancer.ErrBadResolverState { |
| lb.cc.ResolveNow(resolver.ResolveNowOptions{}) |
| } else { |
| lb.logger.Warningf("Error from child policy on receiving initial state: %v", err) |
| } |
| } |
| lb.latestClientConnState = nil |
| } |
| if lb.latestResolverError != nil { |
| lb.delegate.ResolverError(lb.latestResolverError) |
| lb.latestResolverError = nil |
| } |
| } |
| |
| // idlePicker is used when the SubConn is IDLE and kicks the SubConn into |
| // CONNECTING when Pick is called. |
| type idlePicker struct { |
| exitIdle func() |
| } |
| |
| func (i *idlePicker) Pick(balancer.PickInfo) (balancer.PickResult, error) { |
| i.exitIdle() |
| return balancer.PickResult{}, balancer.ErrNoSubConnAvailable |
| } |