go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/analysis/internal/clustering/rules/cache/cache_test.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  	"sort"
    19  	"testing"
    20  	"time"
    21  
    22  	"go.chromium.org/luci/common/clock/testclock"
    23  	"go.chromium.org/luci/server/caching"
    24  
    25  	"go.chromium.org/luci/analysis/internal/bugs"
    26  	"go.chromium.org/luci/analysis/internal/clustering/rules"
    27  	"go.chromium.org/luci/analysis/internal/testutil"
    28  
    29  	. "github.com/smartystreets/goconvey/convey"
    30  	. "go.chromium.org/luci/common/testing/assertions"
    31  )
    32  
    33  var cache = caching.RegisterLRUCache[string, *Ruleset](50)
    34  
    35  func TestRulesCache(t *testing.T) {
    36  	Convey(`With Spanner Test Database`, t, func() {
    37  		ctx := testutil.IntegrationTestContext(t)
    38  		ctx, tc := testclock.UseTime(ctx, testclock.TestRecentTimeUTC)
    39  		ctx = caching.WithEmptyProcessCache(ctx)
    40  
    41  		rc := NewRulesCache(cache)
    42  		err := rules.SetForTesting(ctx, nil)
    43  		So(err, ShouldBeNil)
    44  
    45  		test := func(minimumPredicatesVerison time.Time, expectedRules []*rules.Entry, expectedVersion rules.Version) {
    46  			// Tests the content of the cache is as expected.
    47  			ruleset, err := rc.Ruleset(ctx, "myproject", minimumPredicatesVerison)
    48  			So(err, ShouldBeNil)
    49  			So(ruleset.Version, ShouldResemble, expectedVersion)
    50  
    51  			activeRules := 0
    52  			for _, e := range expectedRules {
    53  				if e.IsActive {
    54  					activeRules++
    55  				}
    56  			}
    57  			So(len(ruleset.ActiveRulesSorted), ShouldEqual, activeRules)
    58  			So(len(ruleset.ActiveRulesByID), ShouldEqual, activeRules)
    59  
    60  			sortedExpectedRules := sortRulesByPredicateLastUpdated(expectedRules)
    61  
    62  			actualRuleIndex := 0
    63  			for _, e := range sortedExpectedRules {
    64  				if e.IsActive {
    65  					a := ruleset.ActiveRulesSorted[actualRuleIndex]
    66  					So(a.Rule, ShouldResembleProto, *e)
    67  					// Technically (*lang.Expr).String() may not get us
    68  					// back the original rule if RuleDefinition didn't use
    69  					// normalised formatting. But for this test, we use
    70  					// normalised formatting, so that is not an issue.
    71  					So(a.Expr, ShouldNotBeNil)
    72  					So(a.Expr.String(), ShouldEqual, e.RuleDefinition)
    73  					actualRuleIndex++
    74  
    75  					a2, ok := ruleset.ActiveRulesByID[a.Rule.RuleID]
    76  					So(ok, ShouldBeTrue)
    77  					So(a2.Rule, ShouldResembleProto, *e)
    78  				}
    79  			}
    80  			So(len(ruleset.ActiveRulesWithPredicateUpdatedSince(rules.StartingEpoch)), ShouldEqual, activeRules)
    81  			So(len(ruleset.ActiveRulesWithPredicateUpdatedSince(time.Date(2100, time.January, 1, 1, 0, 0, 0, time.UTC))), ShouldEqual, 0)
    82  		}
    83  
    84  		Convey(`Initially Empty`, func() {
    85  			err := rules.SetForTesting(ctx, nil)
    86  			So(err, ShouldBeNil)
    87  			test(rules.StartingEpoch, nil, rules.StartingVersion)
    88  
    89  			Convey(`Then Empty`, func() {
    90  				// Test cache.
    91  				test(rules.StartingEpoch, nil, rules.StartingVersion)
    92  
    93  				tc.Add(refreshInterval)
    94  
    95  				test(rules.StartingEpoch, nil, rules.StartingVersion)
    96  				test(rules.StartingEpoch, nil, rules.StartingVersion)
    97  			})
    98  			Convey(`Then Non-Empty`, func() {
    99  				// Spanner commit timestamps are in microsecond
   100  				// (not nanosecond) granularity, and some Spanner timestamp
   101  				// operators truncates to microseconds. For this
   102  				// reason, we use microsecond resolution timestamps
   103  				// when testing.
   104  				reference := time.Date(2020, 1, 2, 3, 4, 5, 6000, time.UTC)
   105  
   106  				rs := []*rules.Entry{
   107  					rules.NewRule(100).
   108  						WithLastUpdateTime(reference.Add(-1 * time.Hour)).
   109  						WithPredicateLastUpdateTime(reference.Add(-2 * time.Hour)).
   110  						Build(),
   111  					rules.NewRule(101).WithActive(false).
   112  						WithLastUpdateTime(reference.Add(1 * time.Hour)).
   113  						WithPredicateLastUpdateTime(reference).
   114  						Build(),
   115  				}
   116  				err := rules.SetForTesting(ctx, rs)
   117  				So(err, ShouldBeNil)
   118  
   119  				expectedRulesVersion := rules.Version{
   120  					Total:      reference.Add(1 * time.Hour),
   121  					Predicates: reference,
   122  				}
   123  
   124  				Convey(`By Strong Read`, func() {
   125  					test(StrongRead, rs, expectedRulesVersion)
   126  					test(StrongRead, rs, expectedRulesVersion)
   127  				})
   128  				Convey(`By Requesting Version`, func() {
   129  					test(expectedRulesVersion.Predicates, rs, expectedRulesVersion)
   130  				})
   131  				Convey(`By Cache Expiry`, func() {
   132  					// Test cache is working and still returning the old value.
   133  					tc.Add(refreshInterval / 2)
   134  					test(rules.StartingEpoch, nil, rules.StartingVersion)
   135  
   136  					tc.Add(refreshInterval)
   137  
   138  					test(rules.StartingEpoch, rs, expectedRulesVersion)
   139  					test(rules.StartingEpoch, rs, expectedRulesVersion)
   140  				})
   141  			})
   142  		})
   143  		Convey(`Initially Non-Empty`, func() {
   144  			reference := time.Date(2021, 1, 2, 3, 4, 5, 6000, time.UTC)
   145  
   146  			ruleOne := rules.NewRule(100).
   147  				WithLastUpdateTime(reference.Add(-2 * time.Hour)).
   148  				WithPredicateLastUpdateTime(reference.Add(-3 * time.Hour))
   149  			ruleTwo := rules.NewRule(101).
   150  				WithLastUpdateTime(reference.Add(-2 * time.Hour)).
   151  				WithPredicateLastUpdateTime(reference.Add(-3 * time.Hour))
   152  			ruleThree := rules.NewRule(102).WithActive(false).
   153  				WithLastUpdateTime(reference).
   154  				WithPredicateLastUpdateTime(reference.Add(-1 * time.Hour))
   155  
   156  			rs := []*rules.Entry{
   157  				ruleOne.Build(),
   158  				ruleTwo.Build(),
   159  				ruleThree.Build(),
   160  			}
   161  			err := rules.SetForTesting(ctx, rs)
   162  			So(err, ShouldBeNil)
   163  
   164  			expectedRulesVersion := rules.Version{
   165  				Total:      reference,
   166  				Predicates: reference.Add(-1 * time.Hour),
   167  			}
   168  			test(rules.StartingEpoch, rs, expectedRulesVersion)
   169  
   170  			Convey(`Then Empty`, func() {
   171  				// Mark all rules inactive.
   172  				newRules := []*rules.Entry{
   173  					ruleOne.WithActive(false).
   174  						WithLastUpdateTime(reference.Add(4 * time.Hour)).
   175  						WithPredicateLastUpdateTime(reference.Add(3 * time.Hour)).
   176  						Build(),
   177  					ruleTwo.WithActive(false).
   178  						WithLastUpdateTime(reference.Add(2 * time.Hour)).
   179  						WithPredicateLastUpdateTime(reference.Add(1 * time.Hour)).
   180  						Build(),
   181  					ruleThree.WithActive(false).
   182  						WithLastUpdateTime(reference.Add(2 * time.Hour)).
   183  						WithPredicateLastUpdateTime(reference.Add(1 * time.Hour)).
   184  						Build(),
   185  				}
   186  				err := rules.SetForTesting(ctx, newRules)
   187  				So(err, ShouldBeNil)
   188  
   189  				oldRulesVersion := expectedRulesVersion
   190  				expectedRulesVersion := rules.Version{
   191  					Total:      reference.Add(4 * time.Hour),
   192  					Predicates: reference.Add(3 * time.Hour),
   193  				}
   194  
   195  				Convey(`By Strong Read`, func() {
   196  					test(StrongRead, newRules, expectedRulesVersion)
   197  					test(StrongRead, newRules, expectedRulesVersion)
   198  				})
   199  				Convey(`By Requesting Version`, func() {
   200  					test(expectedRulesVersion.Predicates, newRules, expectedRulesVersion)
   201  				})
   202  				Convey(`By Cache Expiry`, func() {
   203  					// Test cache is working and still returning the old value.
   204  					tc.Add(refreshInterval / 2)
   205  					test(rules.StartingEpoch, rs, oldRulesVersion)
   206  
   207  					tc.Add(refreshInterval)
   208  
   209  					test(rules.StartingEpoch, newRules, expectedRulesVersion)
   210  					test(rules.StartingEpoch, newRules, expectedRulesVersion)
   211  				})
   212  			})
   213  			Convey(`Then Non-Empty`, func() {
   214  				newRules := []*rules.Entry{
   215  					// Mark an existing rule inactive.
   216  					ruleOne.WithActive(false).
   217  						WithLastUpdateTime(reference.Add(time.Hour)).
   218  						WithPredicateLastUpdateTime(reference.Add(time.Hour)).
   219  						Build(),
   220  					// Make a non-predicate change on an active rule.
   221  					ruleTwo.
   222  						WithBug(bugs.BugID{System: "monorail", ID: "project/123"}).
   223  						WithLastUpdateTime(reference.Add(time.Hour)).
   224  						Build(),
   225  					// Make an existing rule active.
   226  					ruleThree.WithActive(true).
   227  						WithLastUpdateTime(reference.Add(time.Hour)).
   228  						WithPredicateLastUpdateTime(reference.Add(time.Hour)).
   229  						Build(),
   230  					// Add a new active rule.
   231  					rules.NewRule(103).
   232  						WithPredicateLastUpdateTime(reference.Add(time.Hour)).
   233  						WithLastUpdateTime(reference.Add(time.Hour)).
   234  						Build(),
   235  					// Add a new inactive rule.
   236  					rules.NewRule(104).WithActive(false).
   237  						WithPredicateLastUpdateTime(reference.Add(2 * time.Hour)).
   238  						WithLastUpdateTime(reference.Add(3 * time.Hour)).
   239  						Build(),
   240  				}
   241  				err := rules.SetForTesting(ctx, newRules)
   242  				So(err, ShouldBeNil)
   243  
   244  				oldRulesVersion := expectedRulesVersion
   245  				expectedRulesVersion := rules.Version{
   246  					Total:      reference.Add(3 * time.Hour),
   247  					Predicates: reference.Add(2 * time.Hour),
   248  				}
   249  
   250  				Convey(`By Strong Read`, func() {
   251  					test(StrongRead, newRules, expectedRulesVersion)
   252  					test(StrongRead, newRules, expectedRulesVersion)
   253  				})
   254  				Convey(`By Forced Eviction`, func() {
   255  					test(expectedRulesVersion.Predicates, newRules, expectedRulesVersion)
   256  				})
   257  				Convey(`By Cache Expiry`, func() {
   258  					// Test cache is working and still returning the old value.
   259  					tc.Add(refreshInterval / 2)
   260  					test(rules.StartingEpoch, rs, oldRulesVersion)
   261  
   262  					tc.Add(refreshInterval)
   263  
   264  					test(rules.StartingEpoch, newRules, expectedRulesVersion)
   265  					test(rules.StartingEpoch, newRules, expectedRulesVersion)
   266  				})
   267  			})
   268  		})
   269  	})
   270  }
   271  
   272  func sortRulesByPredicateLastUpdated(rs []*rules.Entry) []*rules.Entry {
   273  	result := make([]*rules.Entry, len(rs))
   274  	copy(result, rs)
   275  	sort.Slice(result, func(i, j int) bool {
   276  		if result[i].PredicateLastUpdateTime.Equal(result[j].PredicateLastUpdateTime) {
   277  			return result[i].RuleID < result[j].RuleID
   278  		}
   279  		return result[i].PredicateLastUpdateTime.After(result[j].PredicateLastUpdateTime)
   280  	})
   281  	return result
   282  }