/*
 *
 * Copyright 2022 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 outlierdetection provides an implementation of the outlier detection
// LB policy, as defined in
// https://github.com/grpc/proposal/blob/master/A50-xds-outlier-detection.md.
package outlierdetection

import (
	"encoding/json"
	"fmt"
	"math"
	rand "math/rand/v2"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"google.golang.org/grpc/balancer"
	"google.golang.org/grpc/connectivity"
	estats "google.golang.org/grpc/experimental/stats"
	"google.golang.org/grpc/internal/balancer/gracefulswitch"
	"google.golang.org/grpc/internal/buffer"
	"google.golang.org/grpc/internal/channelz"
	"google.golang.org/grpc/internal/grpclog"
	"google.golang.org/grpc/internal/grpcsync"
	iserviceconfig "google.golang.org/grpc/internal/serviceconfig"
	"google.golang.org/grpc/resolver"
	"google.golang.org/grpc/serviceconfig"
)

// Globals to stub out in tests.
var (
	afterFunc = time.AfterFunc
	now       = time.Now
)

// Name is the name of the outlier detection balancer.
const Name = "outlier_detection_experimental"

var (
	ejectionsEnforcedMetric = estats.RegisterInt64Count(estats.MetricDescriptor{
		Name:        "grpc.lb.outlier_detection.ejections_enforced",
		Description: "EXPERIMENTAL. Number of outlier ejections enforced by detection method",
		Unit:        "{ejection}",
		Labels:      []string{"grpc.target", "grpc.lb.outlier_detection.detection_method"},
		Default:     false,
	})

	ejectionsUnenforcedMetric = estats.RegisterInt64Count(estats.MetricDescriptor{
		Name:        "grpc.lb.outlier_detection.ejections_unenforced",
		Description: "EXPERIMENTAL. Number of unenforced outlier ejections due to either `max_ejection_percentage` or `enforcement_percentage`",
		Unit:        "{ejection}",
		Labels:      []string{"grpc.target", "grpc.lb.outlier_detection.detection_method", "grpc.lb.outlier_detection.unenforced_reason"},
		Default:     false,
	})
)

func init() {
	balancer.Register(bb{})
}

type bb struct{}

func (bb) Build(cc balancer.ClientConn, bOpts balancer.BuildOptions) balancer.Balancer {
	b := &outlierDetectionBalancer{
		ClientConn:      cc,
		closed:          grpcsync.NewEvent(),
		done:            grpcsync.NewEvent(),
		addrs:           make(map[string]*endpointInfo),
		scUpdateCh:      buffer.NewUnbounded[any](),
		pickerUpdateCh:  buffer.NewUnbounded[any](),
		channelzParent:  bOpts.ChannelzParent,
		endpoints:       resolver.NewEndpointMap[*endpointInfo](),
		metricsRecorder: cc.MetricsRecorder(), // we use an explicit field instead of using cc.MetricsRecorder() so we can override the metric recorder in tests.
		target:          bOpts.Target.String(),
	}
	b.logger = prefixLogger(b)
	b.logger.Infof("Created")
	b.child = synchronizingBalancerWrapper{lb: gracefulswitch.NewBalancer(b, bOpts)}
	go b.run()
	return b
}

func (bb) ParseConfig(s json.RawMessage) (serviceconfig.LoadBalancingConfig, error) {
	lbCfg := &LBConfig{
		// Default top layer values as documented in A50.
		Interval:           iserviceconfig.Duration(10 * time.Second),
		BaseEjectionTime:   iserviceconfig.Duration(30 * time.Second),
		MaxEjectionTime:    iserviceconfig.Duration(300 * time.Second),
		MaxEjectionPercent: 10,
	}

	// This unmarshalling handles underlying layers sre and fpe which have their
	// own defaults for their fields if either sre or fpe are present.
	if err := json.Unmarshal(s, lbCfg); err != nil { // Validates child config if present as well.
		return nil, fmt.Errorf("xds: unable to unmarshal LBconfig: %s, error: %v", string(s), err)
	}

	// Note: in the xds flow, these validations will never fail. The xdsclient
	// performs the same validations as here on the xds Outlier Detection
	// resource before parsing resource into JSON which this function gets
	// called with. A50 defines two separate places for these validations to
	// take place, the xdsclient and this ParseConfig method. "When parsing a
	// config from JSON, if any of these requirements is violated, that should
	// be treated as a parsing error." - A50
	switch {
	// "The google.protobuf.Duration fields interval, base_ejection_time, and
	// max_ejection_time must obey the restrictions in the
	// google.protobuf.Duration documentation and they must have non-negative
	// values." - A50
	// Approximately 290 years is the maximum time that time.Duration (int64)
	// can represent. The restrictions on the protobuf.Duration field are to be
	// within +-10000 years. Thus, just check for negative values.
	case lbCfg.Interval < 0:
		return nil, fmt.Errorf("OutlierDetectionLoadBalancingConfig.interval = %s; must be >= 0", lbCfg.Interval)
	case lbCfg.BaseEjectionTime < 0:
		return nil, fmt.Errorf("OutlierDetectionLoadBalancingConfig.base_ejection_time = %s; must be >= 0", lbCfg.BaseEjectionTime)
	case lbCfg.MaxEjectionTime < 0:
		return nil, fmt.Errorf("OutlierDetectionLoadBalancingConfig.max_ejection_time = %s; must be >= 0", lbCfg.MaxEjectionTime)

	// "The fields max_ejection_percent,
	// success_rate_ejection.enforcement_percentage,
	// failure_percentage_ejection.threshold, and
	// failure_percentage.enforcement_percentage must have values less than or
	// equal to 100." - A50
	case lbCfg.MaxEjectionPercent > 100:
		return nil, fmt.Errorf("OutlierDetectionLoadBalancingConfig.max_ejection_percent = %v; must be <= 100", lbCfg.MaxEjectionPercent)
	case lbCfg.SuccessRateEjection != nil && lbCfg.SuccessRateEjection.EnforcementPercentage > 100:
		return nil, fmt.Errorf("OutlierDetectionLoadBalancingConfig.SuccessRateEjection.enforcement_percentage = %v; must be <= 100", lbCfg.SuccessRateEjection.EnforcementPercentage)
	case lbCfg.FailurePercentageEjection != nil && lbCfg.FailurePercentageEjection.Threshold > 100:
		return nil, fmt.Errorf("OutlierDetectionLoadBalancingConfig.FailurePercentageEjection.threshold = %v; must be <= 100", lbCfg.FailurePercentageEjection.Threshold)
	case lbCfg.FailurePercentageEjection != nil && lbCfg.FailurePercentageEjection.EnforcementPercentage > 100:
		return nil, fmt.Errorf("OutlierDetectionLoadBalancingConfig.FailurePercentageEjection.enforcement_percentage = %v; must be <= 100", lbCfg.FailurePercentageEjection.EnforcementPercentage)
	}
	return lbCfg, nil
}

func (bb) Name() string {
	return Name
}

// scUpdate wraps a subConn update to be sent to the child balancer.
type scUpdate struct {
	scw   *subConnWrapper
	state balancer.SubConnState
}

type ejectionUpdate struct {
	scw       *subConnWrapper
	isEjected bool // true for ejected, false for unejected
}

type lbCfgUpdate struct {
	lbCfg *LBConfig
	// to make sure picker is updated synchronously.
	done chan struct{}
}

type scHealthUpdate struct {
	scw   *subConnWrapper
	state balancer.SubConnState
}

type outlierDetectionBalancer struct {
	balancer.ClientConn
	// These fields are safe to be accessed without holding any mutex because
	// they are synchronized in run(), which makes these field accesses happen
	// serially.
	//
	// childState is the latest balancer state received from the child.
	childState balancer.State
	// recentPickerNoop represents whether the most recent picker sent upward to
	// the balancer.ClientConn is a noop picker, which doesn't count RPC's. Used
	// to suppress redundant picker updates.
	recentPickerNoop bool

	closed          *grpcsync.Event
	done            *grpcsync.Event
	logger          *grpclog.PrefixLogger
	channelzParent  channelz.Identifier
	metricsRecorder estats.MetricsRecorder
	target          string

	child synchronizingBalancerWrapper

	// mu guards access to the following fields. It also helps to synchronize
	// behaviors of the following events: config updates, firing of the interval
	// timer, SubConn State updates, SubConn address updates, and child state
	// updates.
	//
	// For example, when we receive a config update in the middle of the
	// interval timer algorithm, which uses knobs present in the config, the
	// balancer will wait for the interval timer algorithm to finish before
	// persisting the new configuration.
	//
	// Another example would be the updating of the endpoints or addrs map, such
	// as from a SubConn address update in the middle of the interval timer
	// algorithm which uses endpoints. This balancer waits for the interval
	// timer algorithm to finish before making the update to the endpoints map.
	//
	// This mutex is never held when calling methods on the child policy
	// (within the context of a single goroutine).
	mu sync.Mutex
	// endpoints stores pointers to endpointInfo objects for each endpoint.
	endpoints *resolver.EndpointMap[*endpointInfo]
	// addrs stores pointers to endpointInfo objects for each address. Addresses
	// belonging to the same endpoint point to the same object.
	addrs                 map[string]*endpointInfo
	cfg                   *LBConfig
	timerStartTime        time.Time
	intervalTimer         *time.Timer
	inhibitPickerUpdates  bool
	updateUnconditionally bool
	numEndpointsEjected   int // For fast calculations of percentage of endpoints ejected

	scUpdateCh     *buffer.Unbounded[any]
	pickerUpdateCh *buffer.Unbounded[any]
}

// noopConfig returns whether this balancer is configured with a logical no-op
// configuration or not.
//
// Caller must hold b.mu.
func (b *outlierDetectionBalancer) noopConfig() bool {
	return b.cfg.SuccessRateEjection == nil && b.cfg.FailurePercentageEjection == nil
}

// onIntervalConfig handles logic required specifically on the receipt of a
// configuration which specifies to count RPC's and periodically perform passive
// health checking based on heuristics defined in configuration every configured
// interval.
//
// Caller must hold b.mu.
func (b *outlierDetectionBalancer) onIntervalConfig() {
	var interval time.Duration
	if b.timerStartTime.IsZero() {
		b.timerStartTime = time.Now()
		for _, epInfo := range b.endpoints.All() {
			epInfo.callCounter.clear()
		}
		interval = time.Duration(b.cfg.Interval)
	} else {
		interval = time.Duration(b.cfg.Interval) - now().Sub(b.timerStartTime)
		if interval < 0 {
			interval = 0
		}
	}
	b.intervalTimer = afterFunc(interval, b.intervalTimerAlgorithm)
}

// onNoopConfig handles logic required specifically on the receipt of a
// configuration which specifies the balancer to be a noop.
//
// Caller must hold b.mu.
func (b *outlierDetectionBalancer) onNoopConfig() {
	// "If a config is provided with both the `success_rate_ejection` and
	// `failure_percentage_ejection` fields unset, skip starting the timer and
	// do the following:"
	// "Unset the timer start timestamp."
	b.timerStartTime = time.Time{}
	for _, epInfo := range b.endpoints.All() {
		// "Uneject all currently ejected endpoints."
		if !epInfo.latestEjectionTimestamp.IsZero() {
			b.unejectEndpoint(epInfo)
		}
		// "Reset each endpoint's ejection time multiplier to 0."
		epInfo.ejectionTimeMultiplier = 0
	}
}

func (b *outlierDetectionBalancer) UpdateClientConnState(s balancer.ClientConnState) error {
	lbCfg, ok := s.BalancerConfig.(*LBConfig)
	if !ok {
		b.logger.Errorf("received config with unexpected type %T: %v", s.BalancerConfig, s.BalancerConfig)
		return balancer.ErrBadResolverState
	}

	// Reject whole config if child policy doesn't exist, don't persist it for
	// later.
	bb := balancer.Get(lbCfg.ChildPolicy.Name)
	if bb == nil {
		return fmt.Errorf("outlier detection: child balancer %q not registered", lbCfg.ChildPolicy.Name)
	}

	// It is safe to read b.cfg here without holding the mutex, as the only
	// write to b.cfg happens later in this function. This function is part of
	// the balancer.Balancer API, so it is guaranteed to be called in a
	// synchronous manner, so it cannot race with this read.
	if b.cfg == nil || b.cfg.ChildPolicy.Name != lbCfg.ChildPolicy.Name {
		if err := b.child.switchTo(bb); err != nil {
			return fmt.Errorf("outlier detection: error switching to child of type %q: %v", lbCfg.ChildPolicy.Name, err)
		}
	}

	b.mu.Lock()
	// Inhibit child picker updates until this UpdateClientConnState() call
	// completes. If needed, a picker update containing the no-op config bit
	// determined from this config and most recent state from the child will be
	// sent synchronously upward at the end of this UpdateClientConnState()
	// call.
	b.inhibitPickerUpdates = true
	b.updateUnconditionally = false
	b.cfg = lbCfg

	newEndpoints := resolver.NewEndpointMap[bool]()
	for _, ep := range s.ResolverState.Endpoints {
		newEndpoints.Set(ep, true)
		if _, ok := b.endpoints.Get(ep); !ok {
			b.endpoints.Set(ep, newEndpointInfo())
		}
	}

	for ep := range b.endpoints.All() {
		if _, ok := newEndpoints.Get(ep); !ok {
			b.endpoints.Delete(ep)
		}
	}

	// populate the addrs map.
	b.addrs = map[string]*endpointInfo{}
	for _, ep := range s.ResolverState.Endpoints {
		epInfo, _ := b.endpoints.Get(ep)
		for _, addr := range ep.Addresses {
			if _, ok := b.addrs[addr.Addr]; ok {
				b.logger.Errorf("Endpoints contain duplicate address %q", addr.Addr)
				continue
			}
			b.addrs[addr.Addr] = epInfo
		}
	}

	if b.intervalTimer != nil {
		b.intervalTimer.Stop()
	}

	if b.noopConfig() {
		b.onNoopConfig()
	} else {
		b.onIntervalConfig()
	}
	b.mu.Unlock()

	err := b.child.updateClientConnState(balancer.ClientConnState{
		ResolverState:  s.ResolverState,
		BalancerConfig: b.cfg.ChildPolicy.Config,
	})

	done := make(chan struct{})
	b.pickerUpdateCh.Put(lbCfgUpdate{
		lbCfg: lbCfg,
		done:  done,
	})
	<-done

	return err
}

func (b *outlierDetectionBalancer) ResolverError(err error) {
	b.child.resolverError(err)
}

func (b *outlierDetectionBalancer) updateSubConnState(scw *subConnWrapper, state balancer.SubConnState) {
	b.mu.Lock()
	defer b.mu.Unlock()
	scw.setLatestConnectivityState(state.ConnectivityState)
	b.scUpdateCh.Put(&scUpdate{
		scw:   scw,
		state: state,
	})
}

func (b *outlierDetectionBalancer) UpdateSubConnState(sc balancer.SubConn, state balancer.SubConnState) {
	b.logger.Errorf("UpdateSubConnState(%v, %+v) called unexpectedly", sc, state)
}

func (b *outlierDetectionBalancer) Close() {
	b.closed.Fire()
	<-b.done.Done()
	b.child.closeLB()

	b.scUpdateCh.Close()
	b.pickerUpdateCh.Close()

	b.mu.Lock()
	defer b.mu.Unlock()
	if b.intervalTimer != nil {
		b.intervalTimer.Stop()
	}
}

func (b *outlierDetectionBalancer) ExitIdle() {
	b.child.exitIdle()
}

// wrappedPicker delegates to the child policy's picker, and when the request
// finishes, it increments the corresponding counter in the map entry referenced
// by the subConnWrapper that was picked. If both the `success_rate_ejection`
// and `failure_percentage_ejection` fields are unset in the configuration, this
// picker will not count.
type wrappedPicker struct {
	childPicker balancer.Picker
	noopPicker  bool
}

func (wp *wrappedPicker) Pick(info balancer.PickInfo) (balancer.PickResult, error) {
	pr, err := wp.childPicker.Pick(info)
	if err != nil {
		return balancer.PickResult{}, err
	}

	done := func(di balancer.DoneInfo) {
		if !wp.noopPicker {
			incrementCounter(pr.SubConn, di)
		}
		if pr.Done != nil {
			pr.Done(di)
		}
	}
	scw, ok := pr.SubConn.(*subConnWrapper)
	if !ok {
		// This can never happen, but check is present for defensive
		// programming.
		logger.Errorf("Picked SubConn from child picker is not a SubConnWrapper")
		return balancer.PickResult{
			SubConn:  pr.SubConn,
			Done:     done,
			Metadata: pr.Metadata,
		}, nil
	}
	return balancer.PickResult{
		SubConn:  scw.SubConn,
		Done:     done,
		Metadata: pr.Metadata,
	}, nil
}

func incrementCounter(sc balancer.SubConn, info balancer.DoneInfo) {
	scw, ok := sc.(*subConnWrapper)
	if !ok {
		// Shouldn't happen, as comes from child
		return
	}

	// After reading callCounter.activeBucket in this picker a swap call can
	// concurrently change what activeBucket points to. A50 says to swap the
	// pointer, which will cause this race to write to deprecated memory the
	// interval timer algorithm will never read, which makes this race alright.
	epInfo := scw.endpointInfo
	if epInfo == nil {
		return
	}
	ab := epInfo.callCounter.activeBucket.Load()

	if info.Err == nil {
		atomic.AddUint32(&ab.numSuccesses, 1)
	} else {
		atomic.AddUint32(&ab.numFailures, 1)
	}
}

func (b *outlierDetectionBalancer) UpdateState(s balancer.State) {
	b.pickerUpdateCh.Put(s)
}

func (b *outlierDetectionBalancer) NewSubConn(addrs []resolver.Address, opts balancer.NewSubConnOptions) (balancer.SubConn, error) {
	oldListener := opts.StateListener
	scw := &subConnWrapper{
		addresses:         addrs,
		scUpdateCh:        b.scUpdateCh,
		listener:          oldListener,
		latestHealthState: balancer.SubConnState{ConnectivityState: connectivity.Connecting},
	}
	opts.StateListener = func(state balancer.SubConnState) { b.updateSubConnState(scw, state) }
	b.mu.Lock()
	defer b.mu.Unlock()
	sc, err := b.ClientConn.NewSubConn(addrs, opts)
	if err != nil {
		return nil, err
	}
	scw.SubConn = sc
	if len(addrs) != 1 {
		return scw, nil
	}
	epInfo, ok := b.addrs[addrs[0].Addr]
	if !ok {
		return scw, nil
	}
	epInfo.sws = append(epInfo.sws, scw)
	scw.endpointInfo = epInfo
	if !epInfo.latestEjectionTimestamp.IsZero() {
		scw.eject()
	}
	return scw, nil
}

func (b *outlierDetectionBalancer) RemoveSubConn(sc balancer.SubConn) {
	b.logger.Errorf("RemoveSubConn(%v) called unexpectedly", sc)
}

// removeSubConnFromEndpointMapEntry removes the scw from its map entry if
// present.
//
// Caller must hold b.mu.
func (b *outlierDetectionBalancer) removeSubConnFromEndpointMapEntry(scw *subConnWrapper) {
	epInfo := scw.endpointInfo
	if epInfo == nil {
		return
	}
	for i, sw := range epInfo.sws {
		if scw == sw {
			epInfo.sws = append(epInfo.sws[:i], epInfo.sws[i+1:]...)
			return
		}
	}
}

func (b *outlierDetectionBalancer) UpdateAddresses(sc balancer.SubConn, _ []resolver.Address) {
	b.logger.Errorf("UpdateAddresses(%v) called unexpectedly", sc)
}

// handleSubConnUpdate stores the recent state and forward the update.
func (b *outlierDetectionBalancer) handleSubConnUpdate(u *scUpdate) {
	scw := u.scw
	scw.clearHealthListener()
	b.child.updateSubConnState(scw, u.state)
	if u.state.ConnectivityState == connectivity.Shutdown {
		b.mu.Lock()
		b.removeSubConnFromEndpointMapEntry(scw)
		b.mu.Unlock()
	}
}

func (b *outlierDetectionBalancer) handleSubConnHealthUpdate(u *scHealthUpdate) {
	b.child.updateSubConnHealthState(u.scw, u.state)
}

// handleEjectedUpdate handles any SubConns that get ejected/unejected, and
// forwards the appropriate corresponding subConnState to the child policy.
func (b *outlierDetectionBalancer) handleEjectedUpdate(u *ejectionUpdate) {
	b.child.handleEjectionUpdate(u)
}

// handleChildStateUpdate forwards the picker update wrapped in a wrapped picker
// with the noop picker bit present.
func (b *outlierDetectionBalancer) handleChildStateUpdate(u balancer.State) {
	b.childState = u
	b.mu.Lock()
	if b.inhibitPickerUpdates {
		// If a child's state is updated during the suppression of child
		// updates, the synchronous handleLBConfigUpdate function with respect
		// to UpdateClientConnState should return a picker unconditionally.
		b.updateUnconditionally = true
		b.mu.Unlock()
		return
	}
	noopCfg := b.noopConfig()
	b.mu.Unlock()
	b.recentPickerNoop = noopCfg
	b.ClientConn.UpdateState(balancer.State{
		ConnectivityState: b.childState.ConnectivityState,
		Picker: &wrappedPicker{
			childPicker: b.childState.Picker,
			noopPicker:  noopCfg,
		},
	})
}

// handleLBConfigUpdate compares whether the new config is a noop config or not,
// to the noop bit in the picker if present. It updates the picker if this bit
// changed compared to the picker currently in use.
func (b *outlierDetectionBalancer) handleLBConfigUpdate(u lbCfgUpdate) {
	lbCfg := u.lbCfg
	noopCfg := lbCfg.SuccessRateEjection == nil && lbCfg.FailurePercentageEjection == nil
	// If the child has sent its first update and this config flips the noop
	// bit compared to the most recent picker update sent upward, then a new
	// picker with this updated bit needs to be forwarded upward. If a child
	// update was received during the suppression of child updates within
	// UpdateClientConnState(), then a new picker needs to be forwarded with
	// this updated state, irregardless of whether this new configuration flips
	// the bit.
	if b.childState.Picker != nil && noopCfg != b.recentPickerNoop || b.updateUnconditionally {
		b.recentPickerNoop = noopCfg
		b.ClientConn.UpdateState(balancer.State{
			ConnectivityState: b.childState.ConnectivityState,
			Picker: &wrappedPicker{
				childPicker: b.childState.Picker,
				noopPicker:  noopCfg,
			},
		})
	}
	b.inhibitPickerUpdates = false
	b.updateUnconditionally = false
	close(u.done)
}

func (b *outlierDetectionBalancer) run() {
	defer b.done.Fire()
	for {
		select {
		case update, ok := <-b.scUpdateCh.Get():
			if !ok {
				return
			}
			b.scUpdateCh.Load()
			if b.closed.HasFired() { // don't send SubConn updates to child after the balancer has been closed
				return
			}
			switch u := update.(type) {
			case *scUpdate:
				b.handleSubConnUpdate(u)
			case *ejectionUpdate:
				b.handleEjectedUpdate(u)
			case *scHealthUpdate:
				b.handleSubConnHealthUpdate(u)
			}
		case update, ok := <-b.pickerUpdateCh.Get():
			if !ok {
				return
			}
			b.pickerUpdateCh.Load()
			if b.closed.HasFired() { // don't send picker updates to grpc after the balancer has been closed
				return
			}
			switch u := update.(type) {
			case balancer.State:
				b.handleChildStateUpdate(u)
			case lbCfgUpdate:
				b.handleLBConfigUpdate(u)
			}
		case <-b.closed.Done():
			return
		}
	}
}

// intervalTimerAlgorithm ejects and unejects endpoints based on the Outlier
// Detection configuration and data about each endpoint from the previous
// interval.
func (b *outlierDetectionBalancer) intervalTimerAlgorithm() {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.timerStartTime = time.Now()

	for _, epInfo := range b.endpoints.All() {
		epInfo.callCounter.swap()
	}

	if b.cfg.SuccessRateEjection != nil {
		b.successRateAlgorithm()
	}

	if b.cfg.FailurePercentageEjection != nil {
		b.failurePercentageAlgorithm()
	}

	for _, epInfo := range b.endpoints.All() {
		if epInfo.latestEjectionTimestamp.IsZero() && epInfo.ejectionTimeMultiplier > 0 {
			epInfo.ejectionTimeMultiplier--
			continue
		}
		if epInfo.latestEjectionTimestamp.IsZero() {
			// Endpoint is already not ejected, so no need to check for whether
			// to uneject the endpoint below.
			continue
		}
		et := time.Duration(b.cfg.BaseEjectionTime) * time.Duration(epInfo.ejectionTimeMultiplier)
		met := max(time.Duration(b.cfg.BaseEjectionTime), time.Duration(b.cfg.MaxEjectionTime))
		uet := epInfo.latestEjectionTimestamp.Add(min(et, met))
		if now().After(uet) {
			b.unejectEndpoint(epInfo)
		}
	}

	// This conditional only for testing (since the interval timer algorithm is
	// called manually), will never hit in production.
	if b.intervalTimer != nil {
		b.intervalTimer.Stop()
	}
	b.intervalTimer = afterFunc(time.Duration(b.cfg.Interval), b.intervalTimerAlgorithm)
}

// endpointsWithAtLeastRequestVolume returns a slice of endpoint information of
// all endpoints with at least request volume passed in.
//
// Caller must hold b.mu.
func (b *outlierDetectionBalancer) endpointsWithAtLeastRequestVolume(requestVolume uint32) []*endpointInfo {
	var endpoints []*endpointInfo
	for _, epInfo := range b.endpoints.All() {
		bucket1 := epInfo.callCounter.inactiveBucket
		rv := bucket1.numSuccesses + bucket1.numFailures
		if rv >= requestVolume {
			endpoints = append(endpoints, epInfo)
		}
	}
	return endpoints
}

// meanAndStdDev returns the mean and std dev of the fractions of successful
// requests of the endpoints passed in.
//
// Caller must hold b.mu.
func (b *outlierDetectionBalancer) meanAndStdDev(endpoints []*endpointInfo) (float64, float64) {
	var totalFractionOfSuccessfulRequests float64
	var mean float64
	for _, epInfo := range endpoints {
		bucket := epInfo.callCounter.inactiveBucket
		rv := bucket.numSuccesses + bucket.numFailures
		totalFractionOfSuccessfulRequests += float64(bucket.numSuccesses) / float64(rv)
	}
	mean = totalFractionOfSuccessfulRequests / float64(len(endpoints))
	var sumOfSquares float64
	for _, epInfo := range endpoints {
		bucket := epInfo.callCounter.inactiveBucket
		rv := bucket.numSuccesses + bucket.numFailures
		devFromMean := (float64(bucket.numSuccesses) / float64(rv)) - mean
		sumOfSquares += devFromMean * devFromMean
	}
	variance := sumOfSquares / float64(len(endpoints))
	return mean, math.Sqrt(variance)
}

// successRateAlgorithm ejects any endpoints where the success rate falls below
// the other endpoints according to mean and standard deviation, and if overall
// applicable from other set heuristics.
//
// Caller must hold b.mu.
func (b *outlierDetectionBalancer) successRateAlgorithm() {
	endpointsToConsider := b.endpointsWithAtLeastRequestVolume(b.cfg.SuccessRateEjection.RequestVolume)
	if len(endpointsToConsider) < int(b.cfg.SuccessRateEjection.MinimumHosts) {
		return
	}
	mean, stddev := b.meanAndStdDev(endpointsToConsider)
	ejectionCfg := b.cfg.SuccessRateEjection
	for _, epInfo := range endpointsToConsider {
		bucket := epInfo.callCounter.inactiveBucket
		successRate := float64(bucket.numSuccesses) / float64(bucket.numSuccesses+bucket.numFailures)
		requiredSuccessRate := mean - stddev*(float64(ejectionCfg.StdevFactor)/1000)
		if successRate < requiredSuccessRate {
			channelz.Infof(logger, b.channelzParent, "SuccessRate algorithm detected outlier: %s. Parameters: successRate=%f, mean=%f, stddev=%f, requiredSuccessRate=%f", epInfo, successRate, mean, stddev, requiredSuccessRate)
			// Check if max ejection percentage would prevent ejection.
			if float64(b.numEndpointsEjected)/float64(b.endpoints.Len())*100 >= float64(b.cfg.MaxEjectionPercent) {
				// Record unenforced ejection due to max ejection percentage.
				ejectionsUnenforcedMetric.Record(b.metricsRecorder, 1, b.target, "success_rate", "max_ejection_overflow")
				continue
			}
			if uint32(rand.Int32N(100)) < ejectionCfg.EnforcementPercentage {
				b.ejectEndpoint(epInfo, "success_rate")
			} else {
				// Record unenforced ejection due to enforcement percentage.
				ejectionsUnenforcedMetric.Record(b.metricsRecorder, 1, b.target, "success_rate", "enforcement_percentage")
			}
		}
	}
}

// failurePercentageAlgorithm ejects any endpoints where the failure percentage
// rate exceeds a set enforcement percentage, if overall applicable from other
// set heuristics.
//
// Caller must hold b.mu.
func (b *outlierDetectionBalancer) failurePercentageAlgorithm() {
	endpointsToConsider := b.endpointsWithAtLeastRequestVolume(b.cfg.FailurePercentageEjection.RequestVolume)
	if len(endpointsToConsider) < int(b.cfg.FailurePercentageEjection.MinimumHosts) {
		return
	}

	ejectionCfg := b.cfg.FailurePercentageEjection
	for _, epInfo := range endpointsToConsider {
		bucket := epInfo.callCounter.inactiveBucket
		failurePercentage := (float64(bucket.numFailures) / float64(bucket.numSuccesses+bucket.numFailures)) * 100
		if failurePercentage > float64(b.cfg.FailurePercentageEjection.Threshold) {
			channelz.Infof(logger, b.channelzParent, "FailurePercentage algorithm detected outlier: %s, failurePercentage=%f", epInfo, failurePercentage)
			// Check if max ejection percentage would prevent ejection.
			if float64(b.numEndpointsEjected)/float64(b.endpoints.Len())*100 >= float64(b.cfg.MaxEjectionPercent) {
				// Record unenforced ejection due to max ejection percentage.
				ejectionsUnenforcedMetric.Record(b.metricsRecorder, 1, b.target, "failure_percentage", "max_ejection_overflow")
				continue
			}
			if uint32(rand.Int32N(100)) < ejectionCfg.EnforcementPercentage {
				b.ejectEndpoint(epInfo, "failure_percentage")
			} else {
				// Record unenforced ejection due to enforcement percentage.
				ejectionsUnenforcedMetric.Record(b.metricsRecorder, 1, b.target, "failure_percentage", "enforcement_percentage")
			}
		}
	}
}

// Caller must hold b.mu.
func (b *outlierDetectionBalancer) ejectEndpoint(epInfo *endpointInfo, detectionMethod string) {
	b.numEndpointsEjected++
	epInfo.latestEjectionTimestamp = b.timerStartTime
	epInfo.ejectionTimeMultiplier++
	for _, sbw := range epInfo.sws {
		sbw.eject()
		channelz.Infof(logger, b.channelzParent, "Subchannel ejected: %s", sbw)
	}

	// Record the enforced ejection metric.
	ejectionsEnforcedMetric.Record(b.metricsRecorder, 1, b.target, detectionMethod)
}

// Caller must hold b.mu.
func (b *outlierDetectionBalancer) unejectEndpoint(epInfo *endpointInfo) {
	b.numEndpointsEjected--
	epInfo.latestEjectionTimestamp = time.Time{}
	for _, sbw := range epInfo.sws {
		sbw.uneject()
		channelz.Infof(logger, b.channelzParent, "Subchannel unejected: %s", sbw)
	}
}

// synchronizingBalancerWrapper serializes calls into balancer (to uphold the
// balancer.Balancer API guarantee of synchronous calls). It also ensures a
// consistent order of locking mutexes when using SubConn listeners to avoid
// deadlocks.
type synchronizingBalancerWrapper struct {
	// mu should not be used directly from outside this struct, instead use
	// methods defined on the struct.
	mu sync.Mutex
	lb *gracefulswitch.Balancer
}

func (sbw *synchronizingBalancerWrapper) switchTo(builder balancer.Builder) error {
	sbw.mu.Lock()
	defer sbw.mu.Unlock()
	return sbw.lb.SwitchTo(builder)
}

func (sbw *synchronizingBalancerWrapper) updateClientConnState(state balancer.ClientConnState) error {
	sbw.mu.Lock()
	defer sbw.mu.Unlock()
	return sbw.lb.UpdateClientConnState(state)
}

func (sbw *synchronizingBalancerWrapper) resolverError(err error) {
	sbw.mu.Lock()
	defer sbw.mu.Unlock()
	sbw.lb.ResolverError(err)
}

func (sbw *synchronizingBalancerWrapper) closeLB() {
	sbw.mu.Lock()
	defer sbw.mu.Unlock()
	sbw.lb.Close()
}

func (sbw *synchronizingBalancerWrapper) exitIdle() {
	sbw.mu.Lock()
	defer sbw.mu.Unlock()
	sbw.lb.ExitIdle()
}

func (sbw *synchronizingBalancerWrapper) updateSubConnHealthState(scw *subConnWrapper, scs balancer.SubConnState) {
	sbw.mu.Lock()
	defer sbw.mu.Unlock()
	scw.updateSubConnHealthState(scs)
}

func (sbw *synchronizingBalancerWrapper) updateSubConnState(scw *subConnWrapper, scs balancer.SubConnState) {
	sbw.mu.Lock()
	defer sbw.mu.Unlock()
	scw.updateSubConnConnectivityState(scs)
}

func (sbw *synchronizingBalancerWrapper) handleEjectionUpdate(u *ejectionUpdate) {
	sbw.mu.Lock()
	defer sbw.mu.Unlock()
	if u.isEjected {
		u.scw.handleEjection()
	} else {
		u.scw.handleUnejection()
	}
}

// endpointInfo contains the runtime information about an endpoint that pertains
// to Outlier Detection. This struct and all of its fields is protected by
// outlierDetectionBalancer.mu in the case where it is accessed through the
// address or endpoint map. In the case of Picker callbacks, the writes to the
// activeBucket of callCounter are protected by atomically loading and storing
// unsafe.Pointers (see further explanation in incrementCounter()).
type endpointInfo struct {
	// The call result counter object.
	callCounter *callCounter

	// The latest ejection timestamp, or zero if the endpoint is currently not
	// ejected.
	latestEjectionTimestamp time.Time

	// The current ejection time multiplier, starting at 0.
	ejectionTimeMultiplier int64

	// A list of subchannel wrapper objects that correspond to this endpoint.
	sws []*subConnWrapper
}

func (a *endpointInfo) String() string {
	var res strings.Builder
	res.WriteString("[")
	for _, sw := range a.sws {
		res.WriteString(sw.String())
	}
	res.WriteString("]")
	return res.String()
}

func newEndpointInfo() *endpointInfo {
	return &endpointInfo{
		callCounter: newCallCounter(),
	}
}
