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  }