google.golang.org/grpc@v1.74.2/xds/internal/clients/lrsclient/load_store.go (about)

     1  /*
     2   *
     3   * Copyright 2025 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 lrsclient
    20  
    21  import (
    22  	"context"
    23  	"sync"
    24  	"sync/atomic"
    25  	"time"
    26  
    27  	"google.golang.org/grpc/xds/internal/clients"
    28  	lrsclientinternal "google.golang.org/grpc/xds/internal/clients/lrsclient/internal"
    29  )
    30  
    31  // A LoadStore aggregates loads for multiple clusters and services that are
    32  // intended to be reported via LRS.
    33  //
    34  // LoadStore stores loads reported to a single LRS server. Use multiple stores
    35  // for multiple servers.
    36  //
    37  // It is safe for concurrent use.
    38  type LoadStore struct {
    39  	// stop is the function to call to Stop the LoadStore reporting.
    40  	stop func(ctx context.Context)
    41  
    42  	// mu only protects the map (2 layers). The read/write to
    43  	// *PerClusterReporter doesn't need to hold the mu.
    44  	mu sync.Mutex
    45  	// clusters is a map with cluster name as the key. The second layer is a
    46  	// map with service name as the key. Each value (PerClusterReporter)
    47  	// contains data for a (cluster, service) pair.
    48  	//
    49  	// Note that new entries are added to this map, but never removed. This is
    50  	// potentially a memory leak. But the memory is allocated for each new
    51  	// (cluster,service) pair, and the memory allocated is just pointers and
    52  	// maps. So this shouldn't get too bad.
    53  	clusters map[string]map[string]*PerClusterReporter
    54  }
    55  
    56  func init() {
    57  	lrsclientinternal.TimeNow = time.Now
    58  }
    59  
    60  // newLoadStore creates a LoadStore.
    61  func newLoadStore() *LoadStore {
    62  	return &LoadStore{
    63  		clusters: make(map[string]map[string]*PerClusterReporter),
    64  	}
    65  }
    66  
    67  // Stop signals the LoadStore to stop reporting.
    68  //
    69  // Before closing the underlying LRS stream, this method may block until a
    70  // final load report send attempt completes or the provided context `ctx`
    71  // expires.
    72  //
    73  // The provided context must have a deadline or timeout set to prevent Stop
    74  // from blocking indefinitely if the final send attempt fails to complete.
    75  //
    76  // Calling Stop on an already stopped LoadStore is a no-op.
    77  func (ls *LoadStore) Stop(ctx context.Context) {
    78  	ls.stop(ctx)
    79  }
    80  
    81  // ReporterForCluster returns the PerClusterReporter for the given cluster and
    82  // service.
    83  func (ls *LoadStore) ReporterForCluster(clusterName, serviceName string) *PerClusterReporter {
    84  	ls.mu.Lock()
    85  	defer ls.mu.Unlock()
    86  	c, ok := ls.clusters[clusterName]
    87  	if !ok {
    88  		c = make(map[string]*PerClusterReporter)
    89  		ls.clusters[clusterName] = c
    90  	}
    91  
    92  	if p, ok := c[serviceName]; ok {
    93  		return p
    94  	}
    95  	p := &PerClusterReporter{
    96  		cluster:          clusterName,
    97  		service:          serviceName,
    98  		lastLoadReportAt: lrsclientinternal.TimeNow(),
    99  	}
   100  	c[serviceName] = p
   101  	return p
   102  }
   103  
   104  // stats returns the load data for the given cluster names. Data is returned in
   105  // a slice with no specific order.
   106  //
   107  // If no clusterName is given (an empty slice), all data for all known clusters
   108  // is returned.
   109  //
   110  // If a cluster's loadData is empty (no load to report), it's not appended to
   111  // the returned slice.
   112  func (ls *LoadStore) stats(clusterNames []string) []*loadData {
   113  	ls.mu.Lock()
   114  	defer ls.mu.Unlock()
   115  
   116  	var ret []*loadData
   117  	if len(clusterNames) == 0 {
   118  		for _, c := range ls.clusters {
   119  			ret = appendClusterStats(ret, c)
   120  		}
   121  		return ret
   122  	}
   123  	for _, n := range clusterNames {
   124  		if c, ok := ls.clusters[n]; ok {
   125  			ret = appendClusterStats(ret, c)
   126  		}
   127  	}
   128  
   129  	return ret
   130  }
   131  
   132  // PerClusterReporter records load data pertaining to a single cluster. It
   133  // provides methods to record call starts, finishes, server-reported loads,
   134  // and dropped calls.
   135  //
   136  // It is safe for concurrent use.
   137  //
   138  // TODO(purnesh42h): Use regular maps with mutexes instead of sync.Map here.
   139  // The latter is optimized for two common use cases: (1) when the entry for a
   140  // given key is only ever written once but read many times, as in caches that
   141  // only grow, or (2) when multiple goroutines read, write, and overwrite
   142  // entries for disjoint sets of keys. In these two cases, use of a Map may
   143  // significantly reduce lock contention compared to a Go map paired with a
   144  // separate Mutex or RWMutex.
   145  // Neither of these conditions are met here, and we should transition to a
   146  // regular map with a mutex for better type safety.
   147  type PerClusterReporter struct {
   148  	cluster, service string
   149  	drops            sync.Map // map[string]*uint64
   150  	localityRPCCount sync.Map // map[clients.Locality]*rpcCountData
   151  
   152  	mu               sync.Mutex
   153  	lastLoadReportAt time.Time
   154  }
   155  
   156  // CallStarted records a call started in the LoadStore.
   157  func (p *PerClusterReporter) CallStarted(locality clients.Locality) {
   158  	s, ok := p.localityRPCCount.Load(locality)
   159  	if !ok {
   160  		tp := newRPCCountData()
   161  		s, _ = p.localityRPCCount.LoadOrStore(locality, tp)
   162  	}
   163  	s.(*rpcCountData).incrInProgress()
   164  	s.(*rpcCountData).incrIssued()
   165  }
   166  
   167  // CallFinished records a call finished in the LoadStore.
   168  func (p *PerClusterReporter) CallFinished(locality clients.Locality, err error) {
   169  	f, ok := p.localityRPCCount.Load(locality)
   170  	if !ok {
   171  		// The map is never cleared, only values in the map are reset. So the
   172  		// case where entry for call-finish is not found should never happen.
   173  		return
   174  	}
   175  	f.(*rpcCountData).decrInProgress()
   176  	if err == nil {
   177  		f.(*rpcCountData).incrSucceeded()
   178  	} else {
   179  		f.(*rpcCountData).incrErrored()
   180  	}
   181  }
   182  
   183  // CallServerLoad records the server load in the LoadStore.
   184  func (p *PerClusterReporter) CallServerLoad(locality clients.Locality, name string, val float64) {
   185  	s, ok := p.localityRPCCount.Load(locality)
   186  	if !ok {
   187  		// The map is never cleared, only values in the map are reset. So the
   188  		// case where entry for callServerLoad is not found should never happen.
   189  		return
   190  	}
   191  	s.(*rpcCountData).addServerLoad(name, val)
   192  }
   193  
   194  // CallDropped records a call dropped in the LoadStore.
   195  func (p *PerClusterReporter) CallDropped(category string) {
   196  	d, ok := p.drops.Load(category)
   197  	if !ok {
   198  		tp := new(uint64)
   199  		d, _ = p.drops.LoadOrStore(category, tp)
   200  	}
   201  	atomic.AddUint64(d.(*uint64), 1)
   202  }
   203  
   204  // stats returns and resets all loads reported to the store, except inProgress
   205  // rpc counts.
   206  //
   207  // It returns nil if the store doesn't contain any (new) data.
   208  func (p *PerClusterReporter) stats() *loadData {
   209  	sd := newLoadData(p.cluster, p.service)
   210  	p.drops.Range(func(key, val any) bool {
   211  		d := atomic.SwapUint64(val.(*uint64), 0)
   212  		if d == 0 {
   213  			return true
   214  		}
   215  		sd.totalDrops += d
   216  		keyStr := key.(string)
   217  		if keyStr != "" {
   218  			// Skip drops without category. They are counted in total_drops, but
   219  			// not in per category. One example is drops by circuit breaking.
   220  			sd.drops[keyStr] = d
   221  		}
   222  		return true
   223  	})
   224  	p.localityRPCCount.Range(func(key, val any) bool {
   225  		countData := val.(*rpcCountData)
   226  		succeeded := countData.loadAndClearSucceeded()
   227  		inProgress := countData.loadInProgress()
   228  		errored := countData.loadAndClearErrored()
   229  		issued := countData.loadAndClearIssued()
   230  		if succeeded == 0 && inProgress == 0 && errored == 0 && issued == 0 {
   231  			return true
   232  		}
   233  
   234  		ld := localityData{
   235  			requestStats: requestData{
   236  				succeeded:  succeeded,
   237  				errored:    errored,
   238  				inProgress: inProgress,
   239  				issued:     issued,
   240  			},
   241  			loadStats: make(map[string]serverLoadData),
   242  		}
   243  		countData.serverLoads.Range(func(key, val any) bool {
   244  			sum, count := val.(*rpcLoadData).loadAndClear()
   245  			if count == 0 {
   246  				return true
   247  			}
   248  			ld.loadStats[key.(string)] = serverLoadData{
   249  				count: count,
   250  				sum:   sum,
   251  			}
   252  			return true
   253  		})
   254  		sd.localityStats[key.(clients.Locality)] = ld
   255  		return true
   256  	})
   257  
   258  	p.mu.Lock()
   259  	sd.reportInterval = lrsclientinternal.TimeNow().Sub(p.lastLoadReportAt)
   260  	p.lastLoadReportAt = lrsclientinternal.TimeNow()
   261  	p.mu.Unlock()
   262  
   263  	if sd.totalDrops == 0 && len(sd.drops) == 0 && len(sd.localityStats) == 0 {
   264  		return nil
   265  	}
   266  	return sd
   267  }
   268  
   269  // loadData contains all load data reported to the LoadStore since the most recent
   270  // call to stats().
   271  type loadData struct {
   272  	// cluster is the name of the cluster this data is for.
   273  	cluster string
   274  	// service is the name of the EDS service this data is for.
   275  	service string
   276  	// totalDrops is the total number of dropped requests.
   277  	totalDrops uint64
   278  	// drops is the number of dropped requests per category.
   279  	drops map[string]uint64
   280  	// localityStats contains load reports per locality.
   281  	localityStats map[clients.Locality]localityData
   282  	// reportInternal is the duration since last time load was reported (stats()
   283  	// was called).
   284  	reportInterval time.Duration
   285  }
   286  
   287  // localityData contains load data for a single locality.
   288  type localityData struct {
   289  	// requestStats contains counts of requests made to the locality.
   290  	requestStats requestData
   291  	// loadStats contains server load data for requests made to the locality,
   292  	// indexed by the load type.
   293  	loadStats map[string]serverLoadData
   294  }
   295  
   296  // requestData contains request counts.
   297  type requestData struct {
   298  	// succeeded is the number of succeeded requests.
   299  	succeeded uint64
   300  	// errored is the number of requests which ran into errors.
   301  	errored uint64
   302  	// inProgress is the number of requests in flight.
   303  	inProgress uint64
   304  	// issued is the total number requests that were sent.
   305  	issued uint64
   306  }
   307  
   308  // serverLoadData contains server load data.
   309  type serverLoadData struct {
   310  	// count is the number of load reports.
   311  	count uint64
   312  	// sum is the total value of all load reports.
   313  	sum float64
   314  }
   315  
   316  // appendClusterStats gets the Data for all the given clusters, append to ret,
   317  // and return the new slice.
   318  //
   319  // Data is only appended to ret if it's not empty.
   320  func appendClusterStats(ret []*loadData, clusters map[string]*PerClusterReporter) []*loadData {
   321  	for _, d := range clusters {
   322  		data := d.stats()
   323  		if data == nil {
   324  			// Skip this data if it doesn't contain any information.
   325  			continue
   326  		}
   327  		ret = append(ret, data)
   328  	}
   329  	return ret
   330  }
   331  
   332  func newLoadData(cluster, service string) *loadData {
   333  	return &loadData{
   334  		cluster:       cluster,
   335  		service:       service,
   336  		drops:         make(map[string]uint64),
   337  		localityStats: make(map[clients.Locality]localityData),
   338  	}
   339  }
   340  
   341  type rpcCountData struct {
   342  	// Only atomic accesses are allowed for the fields.
   343  	succeeded  *uint64
   344  	errored    *uint64
   345  	inProgress *uint64
   346  	issued     *uint64
   347  
   348  	// Map from load desc to load data (sum+count). Loading data from map is
   349  	// atomic, but updating data takes a lock, which could cause contention when
   350  	// multiple RPCs try to report loads for the same desc.
   351  	//
   352  	// To fix the contention, shard this map.
   353  	serverLoads sync.Map // map[string]*rpcLoadData
   354  }
   355  
   356  func newRPCCountData() *rpcCountData {
   357  	return &rpcCountData{
   358  		succeeded:  new(uint64),
   359  		errored:    new(uint64),
   360  		inProgress: new(uint64),
   361  		issued:     new(uint64),
   362  	}
   363  }
   364  
   365  func (rcd *rpcCountData) incrSucceeded() {
   366  	atomic.AddUint64(rcd.succeeded, 1)
   367  }
   368  
   369  func (rcd *rpcCountData) loadAndClearSucceeded() uint64 {
   370  	return atomic.SwapUint64(rcd.succeeded, 0)
   371  }
   372  
   373  func (rcd *rpcCountData) incrErrored() {
   374  	atomic.AddUint64(rcd.errored, 1)
   375  }
   376  
   377  func (rcd *rpcCountData) loadAndClearErrored() uint64 {
   378  	return atomic.SwapUint64(rcd.errored, 0)
   379  }
   380  
   381  func (rcd *rpcCountData) incrInProgress() {
   382  	atomic.AddUint64(rcd.inProgress, 1)
   383  }
   384  
   385  func (rcd *rpcCountData) decrInProgress() {
   386  	atomic.AddUint64(rcd.inProgress, ^uint64(0)) // atomic.Add(x, -1)
   387  }
   388  
   389  func (rcd *rpcCountData) loadInProgress() uint64 {
   390  	return atomic.LoadUint64(rcd.inProgress) // InProgress count is not clear when reading.
   391  }
   392  
   393  func (rcd *rpcCountData) incrIssued() {
   394  	atomic.AddUint64(rcd.issued, 1)
   395  }
   396  
   397  func (rcd *rpcCountData) loadAndClearIssued() uint64 {
   398  	return atomic.SwapUint64(rcd.issued, 0)
   399  }
   400  
   401  func (rcd *rpcCountData) addServerLoad(name string, d float64) {
   402  	loads, ok := rcd.serverLoads.Load(name)
   403  	if !ok {
   404  		tl := newRPCLoadData()
   405  		loads, _ = rcd.serverLoads.LoadOrStore(name, tl)
   406  	}
   407  	loads.(*rpcLoadData).add(d)
   408  }
   409  
   410  // rpcLoadData is data for server loads (from trailers or oob). Fields in this
   411  // struct must be updated consistently.
   412  //
   413  // The current solution is to hold a lock, which could cause contention. To fix,
   414  // shard serverLoads map in rpcCountData.
   415  type rpcLoadData struct {
   416  	mu    sync.Mutex
   417  	sum   float64
   418  	count uint64
   419  }
   420  
   421  func newRPCLoadData() *rpcLoadData {
   422  	return &rpcLoadData{}
   423  }
   424  
   425  func (rld *rpcLoadData) add(v float64) {
   426  	rld.mu.Lock()
   427  	rld.sum += v
   428  	rld.count++
   429  	rld.mu.Unlock()
   430  }
   431  
   432  func (rld *rpcLoadData) loadAndClear() (s float64, c uint64) {
   433  	rld.mu.Lock()
   434  	s, rld.sum = rld.sum, 0
   435  	c, rld.count = rld.count, 0
   436  	rld.mu.Unlock()
   437  	return s, c
   438  }