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 }