/* * * Copyright 2023 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 interop import ( "context" "fmt" "sync" "time" "google.golang.org/grpc/balancer" "google.golang.org/grpc/balancer/endpointsharding" "google.golang.org/grpc/balancer/pickfirst" "google.golang.org/grpc/connectivity" "google.golang.org/grpc/grpclog" internalgrpclog "google.golang.org/grpc/internal/grpclog" "google.golang.org/grpc/orca" "google.golang.org/grpc/resolver" v3orcapb "github.com/cncf/xds/go/xds/data/orca/v3" ) var orcaLogger = grpclog.Component("orca") func init() { balancer.Register(orcabb{}) } type orcabb struct{} // Build creates a new orcab balancer. func (orcabb) Build(cc balancer.ClientConn, bOpts balancer.BuildOptions) balancer.Balancer { b := &orcab{ ClientConn: cc, stopOOBListeners: make(map[balancer.SubConn]func()), oobState: &oobState{ reports: make(map[balancer.SubConn]*v3orcapb.OrcaLoadReport), }, } b.logger = internalgrpclog.NewPrefixLogger(orcaLogger, fmt.Sprintf("[%p] ", b)) b.child = endpointsharding.NewBalancer(b, bOpts, balancer.Get(pickfirst.Name).Build, endpointsharding.Options{}) b.logger.Infof("Created") return b } func (orcabb) Name() string { return "test_backend_metrics_load_balancer" } // orcab is the balancer for the test_backend_metrics_load_balancer policy. // It delegates SubConn management to endpointsharding + pick_first and // intercepts NewSubConn calls to register OOB listeners on READY SubConns. type orcab 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. balancer.ClientConn // Embeds to intercept NewSubConn and UpdateState calls child balancer.Balancer oobState *oobState logger *internalgrpclog.PrefixLogger // mu guards the fields below. mu sync.Mutex stopOOBListeners map[balancer.SubConn]func() } func (b *orcab) UpdateClientConnState(s balancer.ClientConnState) error { // Delegate to the child endpoint sharding balancer, which distributes // state updates to its pick_first children. return b.child.UpdateClientConnState(s) } func (b *orcab) ResolverError(err error) { // Will cause an inline picker update from endpoint sharding. b.child.ResolverError(err) } func (b *orcab) UpdateSubConnState(sc balancer.SubConn, state balancer.SubConnState) { orcaLogger.Errorf("UpdateSubConnState(%v, %+v) called unexpectedly", sc, state) } func (b *orcab) ExitIdle() { // Propagate to the child endpoint sharding balancer. b.child.ExitIdle() } func (b *orcab) Close() { b.mu.Lock() listeners := b.stopOOBListeners b.stopOOBListeners = nil b.mu.Unlock() for _, stop := range listeners { stop() } b.child.Close() } // NewSubConn intercepts SubConn creation from pick_first children to wrap the // StateListener for OOB listener management on READY transitions. func (b *orcab) NewSubConn(addrs []resolver.Address, opts balancer.NewSubConnOptions) (balancer.SubConn, error) { // The variable sc is declared before the closure so the closure captures // the variable, not its (zero) value. After ClientConn.NewSubConn assigns // to sc, the closure sees the real SubConn when invoked. var sc balancer.SubConn oldListener := opts.StateListener opts.StateListener = func(state balancer.SubConnState) { b.updateSubConnState(sc, state) if oldListener != nil { oldListener(state) } } sc, err := b.ClientConn.NewSubConn(addrs, opts) if err != nil { return nil, err } return sc, nil } func (b *orcab) updateSubConnState(sc balancer.SubConn, state balancer.SubConnState) { b.mu.Lock() if b.stopOOBListeners == nil { b.mu.Unlock() return } if state.ConnectivityState == connectivity.Ready { oldStop := b.stopOOBListeners[sc] if oldStop != nil { oldStop() } stop := orca.RegisterOOBListener(sc, &orcaOOBListener{subConn: sc, balancer: b}, orca.OOBListenerOptions{ReportInterval: time.Second}) b.stopOOBListeners[sc] = stop b.mu.Unlock() return } stop := b.stopOOBListeners[sc] if stop != nil { delete(b.stopOOBListeners, sc) defer func() { stop() }() } b.mu.Unlock() if state.ConnectivityState == connectivity.Shutdown { b.oobState.mu.Lock() delete(b.oobState.reports, sc) b.oobState.mu.Unlock() } } // UpdateState intercepts state updates from endpointsharding to wrap the // picker with ORCA load report handling. func (b *orcab) UpdateState(state balancer.State) { // If READY, wrap the picker to inject the ORCA Done callback; otherwise, // pass through the child picker as-is. if state.ConnectivityState == connectivity.Ready { state = balancer.State{ ConnectivityState: state.ConnectivityState, Picker: &orcaPicker{childPicker: state.Picker, oobState: b.oobState}, } } b.ClientConn.UpdateState(state) } type orcaOOBListener struct { subConn balancer.SubConn balancer *orcab } // OnLoadReport implements orca.OOBListener. func (l *orcaOOBListener) OnLoadReport(r *v3orcapb.OrcaLoadReport) { l.balancer.oobState.mu.Lock() defer l.balancer.oobState.mu.Unlock() l.balancer.oobState.reports[l.subConn] = r } type oobState struct { mu sync.Mutex reports map[balancer.SubConn]*v3orcapb.OrcaLoadReport } type orcaPicker struct { childPicker balancer.Picker oobState *oobState } func (p *orcaPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) { res, err := p.childPicker.Pick(info) if err != nil { return res, err } p.oobState.mu.Lock() lr := p.oobState.reports[res.SubConn] p.oobState.mu.Unlock() origDone := res.Done res.Done = func(di balancer.DoneInfo) { perRPCLR, _ := di.ServerLoad.(*v3orcapb.OrcaLoadReport) if perRPCLR != nil && (perRPCLR.CpuUtilization != 0 || perRPCLR.MemUtilization != 0 || len(perRPCLR.Utilization) > 0 || len(perRPCLR.RequestCost) > 0) { setContextCMR(info.Ctx, perRPCLR) } else if lr != nil { setContextCMR(info.Ctx, lr) } if origDone != nil { origDone(di) } } return res, nil } func setContextCMR(ctx context.Context, lr *v3orcapb.OrcaLoadReport) { if r := orcaResultFromContext(ctx); r != nil { *r = lr } } type orcaKey string var orcaCtxKey = orcaKey("orcaResult") // contextWithORCAResult sets a key in ctx with a pointer to an ORCA load // report that is to be filled in by the "test_backend_metrics_load_balancer" // LB policy's Picker's Done callback. // // If a per-call load report is provided from the server for the call, result // will be filled with that, otherwise the most recent OOB load report is used. // If no OOB report has been received, result is not modified. func contextWithORCAResult(ctx context.Context, result **v3orcapb.OrcaLoadReport) context.Context { return context.WithValue(ctx, orcaCtxKey, result) } // orcaResultFromContext returns the ORCA load report stored in the context. // The LB policy uses this to communicate the load report back to the interop // client application. func orcaResultFromContext(ctx context.Context) **v3orcapb.OrcaLoadReport { v := ctx.Value(orcaCtxKey) if v == nil { return nil } return v.(**v3orcapb.OrcaLoadReport) }