go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/rules/cache/cache.go (about) 1 // Copyright 2022 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 cache 16 17 import ( 18 "context" 19 "fmt" 20 "time" 21 22 "go.chromium.org/luci/common/clock" 23 "go.chromium.org/luci/common/data/caching/lru" 24 "go.chromium.org/luci/server/caching" 25 ) 26 27 // refreshInterval controls how often rulesets are refreshed. 28 const refreshInterval = time.Minute 29 30 // StrongRead is a special time used to request the read of a ruleset 31 // that contains all rule changes committed prior to the start of the 32 // read. (Rule changes made after the start of the read may also 33 // be returned.) 34 // Under the covers, this results in a Spanner Strong Read. 35 // See https://cloud.google.com/spanner/docs/reads for more. 36 var StrongRead = time.Unix(0, 0).In(time.FixedZone("RuleCache StrongRead", 0xDB)) 37 38 // RulesCache is an in-process cache of failure association rules used 39 // by LUCI projects. 40 type RulesCache struct { 41 cache caching.LRUHandle[string, *Ruleset] 42 } 43 44 // NewRulesCache initialises a new RulesCache. 45 func NewRulesCache(c caching.LRUHandle[string, *Ruleset]) *RulesCache { 46 return &RulesCache{ 47 cache: c, 48 } 49 } 50 51 // Ruleset obtains the Ruleset for a particular project from the cache, or if 52 // it does not exist, retrieves it from Spanner. MinimumPredicatesVersion 53 // specifies the minimum version of rule predicates that must be incorporated 54 // in the given Ruleset. If no particular version is desired, pass 55 // rules.StartingEpoch. If a strong read is required, pass StrongRead. 56 // Otherwise, pass the particular (minimum) version required. 57 func (c *RulesCache) Ruleset(ctx context.Context, project string, minimumPredicatesVersion time.Time) (*Ruleset, error) { 58 var err error 59 readStart := clock.Now(ctx) 60 61 // Fast path: try and use the existing cached value (if any). 62 ruleset, ok := c.cache.LRU(ctx).Get(ctx, project) 63 if ok { 64 if isRulesetUpToDate(ruleset, readStart, minimumPredicatesVersion) { 65 return ruleset, nil 66 } 67 } 68 69 // Update the cache. This requires acquiring the mutex that 70 // controls updates to the cache entry. 71 ruleset, _ = c.cache.LRU(ctx).Mutate(ctx, project, func(it *lru.Item[*Ruleset]) *lru.Item[*Ruleset] { 72 // Only one goroutine will enter this section at one time. 73 var ruleset *Ruleset 74 if it != nil { 75 ruleset = it.Value 76 if isRulesetUpToDate(ruleset, readStart, minimumPredicatesVersion) { 77 // The ruleset is up-to-date. Do not mutate it further. 78 // This can happen if the ruleset updated while we were 79 // waiting to acquire the mutex to update the cache entry. 80 return it 81 } 82 } else { 83 ruleset = newEmptyRuleset(project) 84 } 85 ruleset, err = ruleset.refresh(ctx) 86 if err != nil { 87 // Issue refreshing ruleset. Keep the cached value (if any) for now. 88 return it 89 } 90 return &lru.Item[*Ruleset]{ 91 Value: ruleset, 92 Exp: 0, // Never. 93 } 94 }) 95 if err != nil { 96 return nil, err 97 } 98 if minimumPredicatesVersion != StrongRead && ruleset.Version.Predicates.Before(minimumPredicatesVersion) { 99 return nil, fmt.Errorf("could not obtain ruleset of requested minimum predicate version (%v)", minimumPredicatesVersion) 100 } 101 return ruleset, nil 102 } 103 104 func isRulesetUpToDate(rs *Ruleset, readStart, minimumPredicatesVersion time.Time) bool { 105 if minimumPredicatesVersion == StrongRead { 106 if rs.LastRefresh.After(readStart) { 107 // We deliberately use a cached ruleset for some strong 108 // reads so long as the refresh occurred after the call to 109 // Ruleset(...). 110 // This is to ensure that even if Ruleset(...) receives 111 // many requests for StrongReads, each will at most need 112 // to wait for the next strong read to complete, rather 113 // than being bottlenecked by the fact only one goroutine 114 // can enter the section to update the cache entry at once. 115 return true 116 } 117 } else { 118 if rs.LastRefresh.Add(refreshInterval).After(readStart) && !rs.Version.Predicates.Before(minimumPredicatesVersion) { 119 return true 120 } 121 } 122 return false 123 }