sigs.k8s.io/prow@v0.0.0-20240503223140-c5e374dc7eb1/pkg/config/cache.go (about) 1 /* 2 Copyright 2021 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package config 18 19 import ( 20 "encoding/json" 21 "errors" 22 "fmt" 23 "time" 24 25 "github.com/prometheus/client_golang/prometheus" 26 "github.com/sirupsen/logrus" 27 28 "sigs.k8s.io/prow/pkg/cache" 29 "sigs.k8s.io/prow/pkg/git/v2" 30 ) 31 32 // Overview 33 // 34 // Consider the expensive function prowYAMLGetter(), which needs to use a Git 35 // client, walk the filesystem path, etc. To speed things up, we save results of 36 // this function into a cache named InRepoConfigCache. 37 38 var inRepoConfigCacheMetrics = struct { 39 // How many times have we looked up an item in this cache? 40 lookups *prometheus.CounterVec 41 // Of the lookups, how many times did we get a cache hit? 42 hits *prometheus.CounterVec 43 // Of the lookups, how many times did we have to construct a cache value 44 // ourselves (cache was useless for this lookup)? 45 misses *prometheus.CounterVec 46 // How many cache key evictions were performed by the underlying LRU 47 // algorithm outside of our control? 48 evictionsForced *prometheus.CounterVec 49 // How many times have we tried to remove a cached key because its value 50 // construction failed? 51 evictionsManual *prometheus.CounterVec 52 // How many entries are in the cache? 53 cacheUsageSize *prometheus.GaugeVec 54 // How long does it take for GetProwYAML() to run? 55 getProwYAMLDuration *prometheus.HistogramVec 56 }{ 57 lookups: prometheus.NewCounterVec(prometheus.CounterOpts{ 58 Name: "inRepoConfigCache_lookups", 59 Help: "Count of cache lookups by org and repo.", 60 }, []string{ 61 "org", 62 "repo", 63 }), 64 hits: prometheus.NewCounterVec(prometheus.CounterOpts{ 65 Name: "inRepoConfigCache_hits", 66 Help: "Count of cache lookup hits by org and repo.", 67 }, []string{ 68 "org", 69 "repo", 70 }), 71 misses: prometheus.NewCounterVec(prometheus.CounterOpts{ 72 Name: "inRepoConfigCache_misses", 73 Help: "Count of cache lookup misses by org and repo.", 74 }, []string{ 75 "org", 76 "repo", 77 }), 78 // Every time we evict a key, record it as a Prometheus metric. This way, we 79 // can monitor how frequently evictions are happening (if it's happening too 80 // frequently, it means that our cache size is too small). 81 evictionsForced: prometheus.NewCounterVec(prometheus.CounterOpts{ 82 Name: "inRepoConfigCache_evictions_forced", 83 Help: "Count of forced cache evictions (due to LRU algorithm) by org and repo.", 84 }, []string{ 85 "org", 86 "repo", 87 }), 88 evictionsManual: prometheus.NewCounterVec(prometheus.CounterOpts{ 89 Name: "inRepoConfigCache_evictions_manual", 90 Help: "Count of manual cache evictions (due to faulty value construction) by org and repo.", 91 }, []string{ 92 "org", 93 "repo", 94 }), 95 cacheUsageSize: prometheus.NewGaugeVec(prometheus.GaugeOpts{ 96 Name: "inRepoConfigCache_cache_usage_size", 97 Help: "Size of the cache (how many entries it is holding) by org and repo.", 98 }, []string{ 99 "org", 100 "repo", 101 }), 102 getProwYAMLDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ 103 Name: "inRepoConfigCache_GetProwYAML_duration", 104 Help: "Histogram of seconds spent retrieving the ProwYAML (inrepoconfig), by org and repo.", 105 Buckets: []float64{0.5, 1, 2, 5, 10, 20, 30, 60, 120, 180, 300, 600}, 106 }, []string{ 107 "org", 108 "repo", 109 }), 110 } 111 112 func init() { 113 prometheus.MustRegister(inRepoConfigCacheMetrics.lookups) 114 prometheus.MustRegister(inRepoConfigCacheMetrics.hits) 115 prometheus.MustRegister(inRepoConfigCacheMetrics.misses) 116 prometheus.MustRegister(inRepoConfigCacheMetrics.evictionsForced) 117 prometheus.MustRegister(inRepoConfigCacheMetrics.evictionsManual) 118 prometheus.MustRegister(inRepoConfigCacheMetrics.cacheUsageSize) 119 prometheus.MustRegister(inRepoConfigCacheMetrics.getProwYAMLDuration) 120 } 121 122 func mkCacheEventCallback(counterVec *prometheus.CounterVec) cache.EventCallback { 123 callback := func(key interface{}) { 124 org, repo, err := keyToOrgRepo(key) 125 if err != nil { 126 return 127 } 128 counterVec.WithLabelValues(org, repo).Inc() 129 } 130 131 return callback 132 } 133 134 // The InRepoConfigCache needs a Config agent client. Here we require that the Agent 135 // type fits the prowConfigAgentClient interface, which requires a Config() 136 // method to retrieve the current Config. Tests can use a fake Config agent 137 // instead of the real one. 138 var _ prowConfigAgentClient = (*Agent)(nil) 139 140 type prowConfigAgentClient interface { 141 Config() *Config 142 } 143 144 // InRepoConfigCache is the user-facing cache. It acts as a wrapper around the 145 // generic LRUCache, by handling type casting in and out of the LRUCache (which 146 // only handles empty interfaces). 147 type InRepoConfigCache struct { 148 *cache.LRUCache 149 configAgent prowConfigAgentClient 150 gitClient git.ClientFactory 151 } 152 153 // NewInRepoConfigCache creates a new LRU cache for ProwYAML values, where the keys 154 // are CacheKeys (that is, JSON strings) and values are pointers to ProwYAMLs. 155 func NewInRepoConfigCache( 156 size int, 157 configAgent prowConfigAgentClient, 158 gitClientFactory git.ClientFactory) (*InRepoConfigCache, error) { 159 160 if gitClientFactory == nil { 161 return nil, fmt.Errorf("InRepoConfigCache requires a non-nil gitClientFactory") 162 } 163 164 lookupsCallback := mkCacheEventCallback(inRepoConfigCacheMetrics.lookups) 165 hitsCallback := mkCacheEventCallback(inRepoConfigCacheMetrics.hits) 166 missesCallback := mkCacheEventCallback(inRepoConfigCacheMetrics.misses) 167 forcedEvictionsCallback := func(key interface{}, _ interface{}) { 168 org, repo, err := keyToOrgRepo(key) 169 if err != nil { 170 return 171 } 172 inRepoConfigCacheMetrics.evictionsForced.WithLabelValues(org, repo).Inc() 173 } 174 manualEvictionsCallback := mkCacheEventCallback(inRepoConfigCacheMetrics.evictionsManual) 175 176 callbacks := cache.Callbacks{ 177 LookupsCallback: lookupsCallback, 178 HitsCallback: hitsCallback, 179 MissesCallback: missesCallback, 180 ForcedEvictionsCallback: forcedEvictionsCallback, 181 ManualEvictionsCallback: manualEvictionsCallback, 182 } 183 184 lruCache, err := cache.NewLRUCache(size, callbacks) 185 if err != nil { 186 return nil, err 187 } 188 189 // This records all OrgRepos we've seen so far during the lifetime of the 190 // process. The main purpose is to allow reporting of 0 counts for OrgRepos 191 // whose keys have been evicted by the lruCache. 192 seenOrgRepos := make(map[OrgRepo]int) 193 194 cacheSizeMetrics := func() { 195 lruCache.Mutex.Lock() // Lock the mutex 196 defer lruCache.Mutex.Unlock() // Unlock the mutex when done 197 // Record all unique orgRepo combinations we've seen so far. 198 for _, key := range lruCache.Keys() { 199 org, repo, err := keyToOrgRepo(key) 200 if err != nil { 201 // This should only happen if we are deliberately using things 202 // other than a CacheKey as the key. 203 logrus.Warnf("programmer error: could not report cache size metrics for a key entry: %v", err) 204 continue 205 } 206 orgRepo := OrgRepo{org, repo} 207 if count, ok := seenOrgRepos[orgRepo]; ok { 208 seenOrgRepos[orgRepo] = count + 1 209 } else { 210 seenOrgRepos[orgRepo] = 1 211 } 212 } 213 // For every single org and repo in the cache, report how many key 214 // entries there are. 215 for orgRepo, count := range seenOrgRepos { 216 inRepoConfigCacheMetrics.cacheUsageSize.WithLabelValues( 217 orgRepo.Org, orgRepo.Repo).Set(float64(count)) 218 // Reset the counter back down to 0 because it may be that by the 219 // time of the next interval, the last key for this orgRepo will be 220 // evicted. At that point we still want to report a count of 0. 221 seenOrgRepos[orgRepo] = 0 222 } 223 } 224 225 go func() { 226 for { 227 cacheSizeMetrics() 228 time.Sleep(30 * time.Second) 229 } 230 }() 231 232 cache := &InRepoConfigCache{ 233 lruCache, 234 // Know how to default the retrieved ProwYAML values against the latest Config. 235 configAgent, 236 // Make the cache be able to handle cache misses (by calling out to Git 237 // to construct the ProwYAML value). 238 gitClientFactory, 239 } 240 241 return cache, nil 242 } 243 244 // CacheKey acts as a key to the InRepoConfigCache. We construct it by marshaling 245 // CacheKeyParts into a JSON string. 246 type CacheKey string 247 248 // The CacheKeyParts is a struct because we want to keep the various components 249 // that make up the key separate to help keep tests readable. Because the 250 // headSHAs field is a slice, the overall CacheKey object is not hashable and 251 // cannot be used directly as a key. Instead we marshal it to JSON first, then 252 // convert its type to CacheKey. 253 // 254 // Users should take care to ensure that headSHAs remains stable (order 255 // matters). 256 type CacheKeyParts struct { 257 Identifier string `json:"identifier"` 258 BaseSHA string `json:"baseSHA"` 259 HeadSHAs []string `json:"headSHAs"` 260 } 261 262 // CacheKey converts a CacheKeyParts object into a JSON string (to be used as a 263 // CacheKey). 264 func (kp *CacheKeyParts) CacheKey() (CacheKey, error) { 265 data, err := json.Marshal(kp) 266 if err != nil { 267 return "", err 268 } 269 270 return CacheKey(data), nil 271 } 272 273 func (cacheKey CacheKey) toCacheKeyParts() (CacheKeyParts, error) { 274 kp := CacheKeyParts{} 275 if err := json.Unmarshal([]byte(cacheKey), &kp); err != nil { 276 return kp, err 277 } 278 return kp, nil 279 } 280 281 func keyToOrgRepo(key interface{}) (string, string, error) { 282 283 cacheKey, ok := key.(CacheKey) 284 if !ok { 285 return "", "", fmt.Errorf("key is not a CacheKey") 286 } 287 288 kp, err := cacheKey.toCacheKeyParts() 289 if err != nil { 290 return "", "", err 291 } 292 293 org, repo, err := SplitRepoName(kp.Identifier) 294 if err != nil { 295 return "", "", err 296 } 297 298 return org, repo, nil 299 } 300 301 // GetPresubmits uses a cache lookup to get the *ProwYAML value (cache hit), 302 // instead of computing it from scratch (cache miss). It also stores the 303 // *ProwYAML into the cache if there is a cache miss. 304 func (cache *InRepoConfigCache) GetPresubmits(identifier, baseBranch string, baseSHAGetter RefGetter, headSHAGetters ...RefGetter) ([]Presubmit, error) { 305 prowYAML, err := cache.GetProwYAML(identifier, baseBranch, baseSHAGetter, headSHAGetters...) 306 if err != nil { 307 return nil, err 308 } 309 310 c := cache.configAgent.Config() 311 return append(c.GetPresubmitsStatic(identifier), prowYAML.Presubmits...), nil 312 } 313 314 // GetPostsubmitsCached is like GetPostsubmits, but attempts to use a cache 315 // lookup to get the *ProwYAML value (cache hit), instead of computing it from 316 // scratch (cache miss). It also stores the *ProwYAML into the cache if there is 317 // a cache miss. 318 func (cache *InRepoConfigCache) GetPostsubmits(identifier, baseBranch string, baseSHAGetter RefGetter, headSHAGetters ...RefGetter) ([]Postsubmit, error) { 319 prowYAML, err := cache.GetProwYAML(identifier, baseBranch, baseSHAGetter, headSHAGetters...) 320 if err != nil { 321 return nil, err 322 } 323 324 c := cache.configAgent.Config() 325 return append(c.GetPostsubmitsStatic(identifier), prowYAML.Postsubmits...), nil 326 } 327 328 // GetProwYAML returns the ProwYAML value stored in the InRepoConfigCache. 329 func (cache *InRepoConfigCache) GetProwYAML(identifier, baseBranch string, baseSHAGetter RefGetter, headSHAGetters ...RefGetter) (*ProwYAML, error) { 330 prowYAML, err := cache.GetProwYAMLWithoutDefaults(identifier, baseBranch, baseSHAGetter, headSHAGetters...) 331 if err != nil { 332 return nil, err 333 } 334 335 c := cache.configAgent.Config() 336 337 // Create a new ProwYAML object based on what we retrieved from the cache. 338 // This way, the act of defaulting values does not modify the elements in 339 // the Presubmits and Postsubmits slices (recall that slices are just 340 // references to areas of memory). This is important for InRepoConfigCache to 341 // behave correctly; otherwise when we default the cached ProwYAML values, 342 // the cached item becomes mutated, affecting future cache lookups. 343 newProwYAML := prowYAML.DeepCopy() 344 if err := DefaultAndValidateProwYAML(c, newProwYAML, identifier); err != nil { 345 return nil, err 346 } 347 348 return newProwYAML, nil 349 } 350 351 func (cache *InRepoConfigCache) GetProwYAMLWithoutDefaults(identifier, baseBranch string, baseSHAGetter RefGetter, headSHAGetters ...RefGetter) (*ProwYAML, error) { 352 timeGetProwYAML := time.Now() 353 defer func() { 354 orgRepo := NewOrgRepo(identifier) 355 inRepoConfigCacheMetrics.getProwYAMLDuration.WithLabelValues(orgRepo.Org, orgRepo.Repo).Observe((float64(time.Since(timeGetProwYAML).Seconds()))) 356 }() 357 358 c := cache.configAgent.Config() 359 360 prowYAML, err := cache.getProwYAML(c.getProwYAML, identifier, baseBranch, baseSHAGetter, headSHAGetters...) 361 if err != nil { 362 return nil, err 363 } 364 365 return prowYAML, nil 366 } 367 368 // GetInRepoConfig just wraps around GetProwYAML(). 369 func (cache *InRepoConfigCache) GetInRepoConfig(identifier, baseBranch string, baseSHAGetter RefGetter, headSHAGetters ...RefGetter) (*ProwYAML, error) { 370 return cache.GetProwYAML(identifier, baseBranch, baseSHAGetter, headSHAGetters...) 371 } 372 373 // valConstructorHelper is called to construct ProwYAML values inside the cache. 374 type valConstructorHelper func( 375 gitClient git.ClientFactory, 376 identifier string, 377 baseBranch string, 378 baseSHAGetter RefGetter, 379 headSHAGetters ...RefGetter, 380 ) (*ProwYAML, error) 381 382 // getProwYAML performs a lookup of previously-calculated *ProwYAML objects. The 383 // 'valConstructorHelper' is used in two ways. First it is used by the caching 384 // mechanism to lazily generate the value only when it is required (otherwise, 385 // if all threads had to generate the value, it would defeat the purpose of the 386 // cache in the first place). Second, it makes it easier to test this function, 387 // because unit tests can just provide its own function for constructing a 388 // *ProwYAML object (instead of needing to create an actual Git repo, etc.). 389 func (cache *InRepoConfigCache) getProwYAML( 390 valConstructorHelper valConstructorHelper, 391 identifier string, 392 baseBranch string, 393 baseSHAGetter RefGetter, 394 headSHAGetters ...RefGetter) (*ProwYAML, error) { 395 396 if identifier == "" { 397 return nil, errors.New("no identifier for repo given") 398 } 399 400 // Abort if the InRepoConfig is not enabled for this identifier (org/repo). 401 // It's important that we short-circuit here __before__ calling cache.Get() 402 // because we do NOT want to add an empty &ProwYAML{} value in the cache 403 // (because not only is it useless, but adding a useless entry also may 404 // result in evicting a useful entry if the underlying cache is full and an 405 // older (useful) key is evicted). 406 c := cache.configAgent.Config() 407 if !c.InRepoConfigEnabled(identifier) { 408 logrus.WithField("identifier", identifier).Debug("Inrepoconfig not enabled, skipping getting prow yaml.") 409 return &ProwYAML{}, nil 410 } 411 412 baseSHA, headSHAs, err := GetAndCheckRefs(baseSHAGetter, headSHAGetters...) 413 if err != nil { 414 return nil, err 415 } 416 417 valConstructor := func() (interface{}, error) { 418 return valConstructorHelper(cache.gitClient, identifier, baseBranch, baseSHAGetter, headSHAGetters...) 419 } 420 421 got, err := cache.get(CacheKeyParts{Identifier: identifier, BaseSHA: baseSHA, HeadSHAs: headSHAs}, valConstructor) 422 if err != nil { 423 return nil, err 424 } 425 426 return got, err 427 } 428 429 // get is a type assertion wrapper around the values retrieved from the inner 430 // LRUCache object (which only understands empty interfaces for both keys and 431 // values). It wraps around the low-level GetOrAdd function. Users are expected 432 // to add their own get method for their own cached value. 433 func (cache *InRepoConfigCache) get( 434 keyParts CacheKeyParts, 435 valConstructor cache.ValConstructor) (*ProwYAML, error) { 436 437 key, err := keyParts.CacheKey() 438 if err != nil { 439 return nil, fmt.Errorf("converting CacheKeyParts to CacheKey: %v", err) 440 } 441 442 now := time.Now() 443 val, cacheHit, err := cache.GetOrAdd(key, valConstructor) 444 if err != nil { 445 return nil, err 446 } 447 logrus.WithFields(logrus.Fields{ 448 "identifier": keyParts.Identifier, 449 "key": key, 450 "duration(seconds)": -time.Until(now).Seconds(), 451 "cache_hit": cacheHit, 452 }).Debug("Duration for resolving inrepoconfig cache.") 453 454 prowYAML, ok := val.(*ProwYAML) 455 if ok { 456 return prowYAML, err 457 } 458 459 // Somehow, the value retrieved with GetOrAdd has the wrong type. This can 460 // happen if some other function modified the cache and put in the wrong 461 // type. Ultimately, this is a price we pay for using a cache library that 462 // uses "interface{}" for the type of its items. 463 err = fmt.Errorf("Programmer error: expected value type '*config.ProwYAML', got '%T'", val) 464 logrus.Error(err) 465 return nil, err 466 }