go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/tsmon/store/inmemory.go (about)

     1  // Copyright 2015 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package store
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"reflect"
    21  	"sync"
    22  	"sync/atomic"
    23  	"time"
    24  
    25  	"go.chromium.org/luci/common/clock"
    26  	"go.chromium.org/luci/common/logging"
    27  	"go.chromium.org/luci/common/tsmon/distribution"
    28  	"go.chromium.org/luci/common/tsmon/field"
    29  	"go.chromium.org/luci/common/tsmon/target"
    30  	"go.chromium.org/luci/common/tsmon/types"
    31  )
    32  
    33  type inMemoryStore struct {
    34  	defaultTarget atomic.Value
    35  
    36  	data     map[dataKey]*metricData
    37  	dataLock sync.RWMutex
    38  }
    39  
    40  type cellKey struct {
    41  	fieldValuesHash, targetHash uint64
    42  }
    43  
    44  type dataKey struct {
    45  	metricName string
    46  	targetType types.TargetType
    47  }
    48  
    49  type metricData struct {
    50  	types.MetricInfo
    51  	types.MetricMetadata
    52  
    53  	cells map[cellKey][]*types.CellData
    54  	lock  sync.Mutex
    55  }
    56  
    57  // findTarget returns the target instance for the given metric.
    58  func (s *inMemoryStore) findTarget(ctx context.Context, m *metricData) types.Target {
    59  	dt := s.DefaultTarget()
    60  
    61  	/* If the type of the metric is NilType or the type of the default target,
    62  	   then this function returns the target, in the context, matched with
    63  	   the type of the default target.
    64  
    65  	   If there is no target found in the context matched with the type, then
    66  	   nil will be returned, and therefore, the target of newly added cells
    67  	   will be determined at the time of store.GetAll() being invoked.
    68  
    69  	   e.g.,
    70  	   // create different targets.
    71  	   target1, target2, target3 := target.Task(...), ....
    72  	   metric := NewInt(...)
    73  
    74  	   // Set the default target with target1.
    75  	   store.SetDefaultTarget(target1)
    76  	   // This creates a new cell with Nil target. It means that the target of
    77  	   // the new cell has not been determined yet. In other words,
    78  	   // SetDefaultTarget() doesn't always guarantee that all the new cells
    79  	   // created after the SetDefaultTarget() invocation will have the target.
    80  	   metric.Set(ctx, 42)
    81  
    82  	   // Create a target context with target2.
    83  	   tctx := target.Set(target2)
    84  	   // This creates a new cell with target2.
    85  	   metric.Incr(tctx, 43)
    86  
    87  	   // Set the default target with target3.
    88  	   SetDefaultTarget(target3)
    89  
    90  	   // This will return cells with the following elements.
    91  	   // - value(42), target(target3)
    92  	   // - value(43), target(target2)
    93  	   cells := store.GetAll()
    94  	*/
    95  	if m.TargetType == target.NilType || m.TargetType == dt.Type() {
    96  		return target.Get(ctx, dt.Type())
    97  	}
    98  
    99  	ct := target.Get(ctx, m.TargetType)
   100  	if ct != nil {
   101  		return ct
   102  	}
   103  	panic(fmt.Sprintf(
   104  		"Missing target for Metric %s with TargetType %s", m.Name, m.TargetType,
   105  	))
   106  }
   107  
   108  func (m *metricData) get(fieldVals []any, t types.Target, resetTime time.Time) *types.CellData {
   109  	fieldVals, err := field.Canonicalize(m.Fields, fieldVals)
   110  	if err != nil {
   111  		panic(err) // bad field types, can only happen if the code is wrong
   112  	}
   113  
   114  	key := cellKey{fieldValuesHash: field.Hash(fieldVals)}
   115  	if t != nil {
   116  		key.targetHash = t.Hash()
   117  	}
   118  
   119  	cells, ok := m.cells[key]
   120  	if ok {
   121  		for _, cell := range cells {
   122  			if reflect.DeepEqual(fieldVals, cell.FieldVals) &&
   123  				reflect.DeepEqual(t, cell.Target) {
   124  				return cell
   125  			}
   126  		}
   127  	}
   128  
   129  	cell := &types.CellData{fieldVals, t, resetTime, nil}
   130  	m.cells[key] = append(cells, cell)
   131  	return cell
   132  }
   133  
   134  func (m *metricData) del(fieldVals []any, t types.Target) {
   135  	fieldVals, err := field.Canonicalize(m.Fields, fieldVals)
   136  	if err != nil {
   137  		panic(err) // bad field types, can only happen if the code is wrong
   138  	}
   139  
   140  	key := cellKey{fieldValuesHash: field.Hash(fieldVals)}
   141  	if t != nil {
   142  		key.targetHash = t.Hash()
   143  	}
   144  
   145  	cells, ok := m.cells[key]
   146  	if ok {
   147  		for i, cell := range cells {
   148  			if reflect.DeepEqual(fieldVals, cell.FieldVals) &&
   149  				reflect.DeepEqual(t, cell.Target) {
   150  				// if the length is 1, then simply delete the map entry.
   151  				if len(cells) == 1 {
   152  					delete(m.cells, key)
   153  					return
   154  				}
   155  				cells[i] = cells[len(cells)-1]
   156  				m.cells[key] = cells[:len(cells)-1]
   157  				return
   158  			}
   159  		}
   160  	}
   161  	return
   162  }
   163  
   164  // NewInMemory creates a new metric store that holds metric data in this
   165  // process' memory.
   166  func NewInMemory(defaultTarget types.Target) Store {
   167  	s := &inMemoryStore{
   168  		data: map[dataKey]*metricData{},
   169  	}
   170  	s.SetDefaultTarget(defaultTarget)
   171  	return s
   172  }
   173  
   174  func (s *inMemoryStore) getOrCreateData(m types.Metric) *metricData {
   175  	dk := dataKey{m.Info().Name, m.Info().TargetType}
   176  	s.dataLock.RLock()
   177  	d, ok := s.data[dk]
   178  	s.dataLock.RUnlock()
   179  	if ok {
   180  		return d
   181  	}
   182  
   183  	s.dataLock.Lock()
   184  	defer s.dataLock.Unlock()
   185  
   186  	// Check again in case another goroutine got the lock before us.
   187  	if d, ok = s.data[dk]; ok {
   188  		return d
   189  	}
   190  
   191  	d = &metricData{
   192  		MetricInfo:     m.Info(),
   193  		MetricMetadata: m.Metadata(),
   194  		cells:          map[cellKey][]*types.CellData{},
   195  	}
   196  
   197  	s.data[dk] = d
   198  	return d
   199  }
   200  
   201  func (s *inMemoryStore) DefaultTarget() types.Target {
   202  	return s.defaultTarget.Load().(types.Target)
   203  }
   204  
   205  func (s *inMemoryStore) SetDefaultTarget(t types.Target) {
   206  	s.defaultTarget.Store(t.Clone())
   207  }
   208  
   209  // Get returns the value for a given metric cell.
   210  func (s *inMemoryStore) Get(ctx context.Context, h types.Metric, resetTime time.Time, fieldVals []any) any {
   211  	if resetTime.IsZero() {
   212  		resetTime = clock.Now(ctx)
   213  	}
   214  
   215  	m := s.getOrCreateData(h)
   216  	t := s.findTarget(ctx, m)
   217  	m.lock.Lock()
   218  	defer m.lock.Unlock()
   219  	return m.get(fieldVals, t, resetTime).Value
   220  }
   221  
   222  func isLessThan(a, b any) bool {
   223  	if a == nil || b == nil {
   224  		return false
   225  	}
   226  	switch a.(type) {
   227  	case int64:
   228  		return a.(int64) < b.(int64)
   229  	case float64:
   230  		return a.(float64) < b.(float64)
   231  	}
   232  	return false
   233  }
   234  
   235  // Set writes the value into the given metric cell.
   236  func (s *inMemoryStore) Set(ctx context.Context, h types.Metric, resetTime time.Time, fieldVals []any, value any) {
   237  	if resetTime.IsZero() {
   238  		resetTime = clock.Now(ctx)
   239  	}
   240  	m := s.getOrCreateData(h)
   241  	t := s.findTarget(ctx, m)
   242  	m.lock.Lock()
   243  	defer m.lock.Unlock()
   244  
   245  	c := m.get(fieldVals, t, resetTime)
   246  
   247  	if m.ValueType.IsCumulative() && isLessThan(value, c.Value) {
   248  		logging.Errorf(ctx,
   249  			"Attempted to set cumulative metric %q to %v, which is lower than the previous value %v",
   250  			h.Info().Name, value, c.Value)
   251  		return
   252  	}
   253  
   254  	c.Value = value
   255  }
   256  
   257  // Del deletes the metric cell.
   258  func (s *inMemoryStore) Del(ctx context.Context, h types.Metric, fieldVals []any) {
   259  	m := s.getOrCreateData(h)
   260  	t := s.findTarget(ctx, m)
   261  	m.lock.Lock()
   262  	defer m.lock.Unlock()
   263  	m.del(fieldVals, t)
   264  	return
   265  }
   266  
   267  // Incr increments the value in a given metric cell by the given delta.
   268  func (s *inMemoryStore) Incr(ctx context.Context, h types.Metric, resetTime time.Time, fieldVals []any, delta any) {
   269  	if resetTime.IsZero() {
   270  		resetTime = clock.Now(ctx)
   271  	}
   272  	m := s.getOrCreateData(h)
   273  	t := s.findTarget(ctx, m)
   274  	m.lock.Lock()
   275  	defer m.lock.Unlock()
   276  
   277  	c := m.get(fieldVals, t, resetTime)
   278  
   279  	switch m.ValueType {
   280  	case types.CumulativeDistributionType:
   281  		d, ok := delta.(float64)
   282  		if !ok {
   283  			panic(fmt.Errorf("Incr got a delta of unsupported type (%v)", delta))
   284  		}
   285  		v, ok := c.Value.(*distribution.Distribution)
   286  		if !ok {
   287  			v = distribution.New(h.(types.DistributionMetric).Bucketer())
   288  			c.Value = v
   289  		}
   290  		v.Add(float64(d))
   291  
   292  	case types.CumulativeIntType:
   293  		if v, ok := c.Value.(int64); ok {
   294  			c.Value = v + delta.(int64)
   295  		} else {
   296  			c.Value = delta.(int64)
   297  		}
   298  
   299  	case types.CumulativeFloatType:
   300  		if v, ok := c.Value.(float64); ok {
   301  			c.Value = v + delta.(float64)
   302  		} else {
   303  			c.Value = delta.(float64)
   304  		}
   305  
   306  	default:
   307  		panic(fmt.Errorf("attempted to increment non-cumulative metric %s by %v", m.Name, delta))
   308  	}
   309  }
   310  
   311  // GetAll efficiently returns all cells in the store.
   312  func (s *inMemoryStore) GetAll(ctx context.Context) []types.Cell {
   313  	s.dataLock.Lock()
   314  	defer s.dataLock.Unlock()
   315  
   316  	defaultTarget := s.DefaultTarget()
   317  
   318  	var ret []types.Cell
   319  	for _, m := range s.data {
   320  		m.lock.Lock()
   321  		for _, ds := range m.cells {
   322  			for _, d := range ds {
   323  				dCopy := *d
   324  
   325  				// Distribution has a slice field. Hence, use Clone() to make a deep copy.
   326  				if d, ok := dCopy.Value.(*distribution.Distribution); ok {
   327  					dCopy.Value = d.Clone()
   328  				}
   329  
   330  				// Add the default target to the cell if it doesn't have one set.
   331  				if dCopy.Target == nil {
   332  					dCopy.Target = defaultTarget
   333  				}
   334  
   335  				cell := types.Cell{
   336  					MetricInfo:     m.MetricInfo,
   337  					MetricMetadata: m.MetricMetadata,
   338  					CellData:       dCopy}
   339  
   340  				ret = append(ret, cell)
   341  			}
   342  		}
   343  		m.lock.Unlock()
   344  	}
   345  	return ret
   346  }
   347  
   348  func (s *inMemoryStore) Reset(ctx context.Context, h types.Metric) {
   349  	m := s.getOrCreateData(h)
   350  
   351  	m.lock.Lock()
   352  	m.cells = make(map[cellKey][]*types.CellData)
   353  	m.lock.Unlock()
   354  }