go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/caching/layered/layered_test.go (about)

     1  // Copyright 2017 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 layered
    16  
    17  import (
    18  	"bytes"
    19  	"context"
    20  	"errors"
    21  	"fmt"
    22  	"math/rand"
    23  	"testing"
    24  	"time"
    25  
    26  	"go.chromium.org/luci/common/clock/testclock"
    27  	"go.chromium.org/luci/common/data/rand/mathrand"
    28  
    29  	"go.chromium.org/luci/server/caching"
    30  	"go.chromium.org/luci/server/caching/cachingtest"
    31  
    32  	. "github.com/smartystreets/goconvey/convey"
    33  	. "go.chromium.org/luci/common/testing/assertions"
    34  )
    35  
    36  var testingCache = RegisterCache(Parameters[[]byte]{
    37  	GlobalNamespace: "namespace",
    38  	Marshal: func(item []byte) ([]byte, error) {
    39  		return item, nil
    40  	},
    41  	Unmarshal: func(blob []byte) ([]byte, error) {
    42  		return blob, nil
    43  	},
    44  })
    45  
    46  var testingCacheWithFallback = RegisterCache(Parameters[[]byte]{
    47  	GlobalNamespace: "namespace",
    48  	Marshal: func(item []byte) ([]byte, error) {
    49  		return item, nil
    50  	},
    51  	Unmarshal: func(blob []byte) ([]byte, error) {
    52  		return blob, nil
    53  	},
    54  	AllowNoProcessCacheFallback: true,
    55  })
    56  
    57  func TestCache(t *testing.T) {
    58  	t.Parallel()
    59  
    60  	Convey("Without process cache", t, func() {
    61  		ctx := context.Background()
    62  
    63  		_, err := testingCache.GetOrCreate(ctx, "item", func() ([]byte, time.Duration, error) {
    64  			panic("should not be called")
    65  		})
    66  		So(err, ShouldEqual, caching.ErrNoProcessCache)
    67  
    68  		calls := 0
    69  		_, err = testingCacheWithFallback.GetOrCreate(ctx, "item", func() ([]byte, time.Duration, error) {
    70  			calls += 1
    71  			return nil, 0, nil
    72  		})
    73  		So(err, ShouldBeNil)
    74  		So(calls, ShouldEqual, 1)
    75  	})
    76  
    77  	Convey("With fake time", t, func() {
    78  		ctx := context.Background()
    79  		ctx = mathrand.Set(ctx, rand.New(rand.NewSource(12345)))
    80  		ctx, tc := testclock.UseTime(ctx, time.Date(2017, time.January, 1, 0, 0, 0, 0, time.UTC))
    81  		ctx = caching.WithEmptyProcessCache(ctx)
    82  
    83  		calls := 0
    84  		value := []byte("value")
    85  		anotherValue := []byte("anotherValue")
    86  
    87  		getter := func() ([]byte, time.Duration, error) {
    88  			calls++
    89  			return value, time.Hour, nil
    90  		}
    91  
    92  		Convey("Without global cache", func() {
    93  			item, err := testingCache.GetOrCreate(ctx, "item", getter)
    94  			So(err, ShouldBeNil)
    95  			So(item, ShouldResemble, value)
    96  			So(calls, ShouldEqual, 1)
    97  
    98  			tc.Add(59 * time.Minute)
    99  
   100  			item, err = testingCache.GetOrCreate(ctx, "item", getter)
   101  			So(err, ShouldBeNil)
   102  			So(item, ShouldResemble, value)
   103  			So(calls, ShouldEqual, 1) // no new calls
   104  
   105  			tc.Add(2 * time.Minute) // cached item expires
   106  
   107  			item, err = testingCache.GetOrCreate(ctx, "item", getter)
   108  			So(err, ShouldBeNil)
   109  			So(item, ShouldResemble, value)
   110  			So(calls, ShouldEqual, 2) // new call!
   111  		})
   112  
   113  		Convey("With global cache", func() {
   114  			global := cachingtest.NewBlobCache()
   115  			ctx = cachingtest.WithGlobalCache(ctx, map[string]caching.BlobCache{
   116  				"namespace": global,
   117  			})
   118  
   119  			Convey("Getting from the global cache", func() {
   120  				// The global cache is empty.
   121  				So(global.LRU.Len(), ShouldEqual, 0)
   122  
   123  				// Create an item.
   124  				item, err := testingCache.GetOrCreate(ctx, "item", getter)
   125  				So(err, ShouldBeNil)
   126  				So(item, ShouldResemble, value)
   127  				So(calls, ShouldEqual, 1)
   128  
   129  				// It is in the global cache now.
   130  				So(global.LRU.Len(), ShouldEqual, 1)
   131  
   132  				// Clear the local cache.
   133  				ctx = caching.WithEmptyProcessCache(ctx)
   134  
   135  				// Grab the item again. Will be fetched from the global cache.
   136  				item, err = testingCache.GetOrCreate(ctx, "item", getter)
   137  				So(err, ShouldBeNil)
   138  				So(item, ShouldResemble, value)
   139  				So(calls, ShouldEqual, 1) // no new calls
   140  			})
   141  
   142  			Convey("Broken global cache is ignored", func() {
   143  				global.Err = errors.New("broken")
   144  
   145  				// Create an item.
   146  				item, err := testingCache.GetOrCreate(ctx, "item", getter)
   147  				So(err, ShouldBeNil)
   148  				So(item, ShouldResemble, value)
   149  				So(calls, ShouldEqual, 1)
   150  
   151  				// Clear the local cache.
   152  				ctx = caching.WithEmptyProcessCache(ctx)
   153  
   154  				// Grab the item again. Will be recreated again, since the global cache
   155  				// is broken.
   156  				item, err = testingCache.GetOrCreate(ctx, "item", getter)
   157  				So(err, ShouldBeNil)
   158  				So(item, ShouldResemble, value)
   159  				So(calls, ShouldEqual, 2) // new call!
   160  			})
   161  		})
   162  
   163  		Convey("Never expiring item", func() {
   164  			item, err := testingCache.GetOrCreate(ctx, "item", func() ([]byte, time.Duration, error) {
   165  				return value, 0, nil
   166  			})
   167  			So(err, ShouldBeNil)
   168  			So(item, ShouldResemble, value)
   169  
   170  			tc.Add(100 * time.Hour)
   171  
   172  			item, err = testingCache.GetOrCreate(ctx, "item", func() ([]byte, time.Duration, error) {
   173  				return nil, 0, errors.New("must not be called")
   174  			})
   175  			So(err, ShouldBeNil)
   176  			So(item, ShouldResemble, value)
   177  		})
   178  
   179  		Convey("WithMinTTL works", func() {
   180  			item, err := testingCache.GetOrCreate(ctx, "item", func() ([]byte, time.Duration, error) {
   181  				return value, time.Hour, nil
   182  			})
   183  			So(err, ShouldBeNil)
   184  			So(item, ShouldResemble, value)
   185  
   186  			tc.Add(50 * time.Minute)
   187  
   188  			// 9 min minTTL is still ok.
   189  			item, err = testingCache.GetOrCreate(ctx, "item", func() ([]byte, time.Duration, error) {
   190  				return nil, 0, errors.New("must not be called")
   191  			}, WithMinTTL(9*time.Minute))
   192  			So(err, ShouldBeNil)
   193  			So(item, ShouldResemble, value)
   194  
   195  			// But 10 min is not and the item is refreshed.
   196  			item, err = testingCache.GetOrCreate(ctx, "item", func() ([]byte, time.Duration, error) {
   197  				return anotherValue, time.Hour, nil
   198  			}, WithMinTTL(10*time.Minute))
   199  			So(err, ShouldBeNil)
   200  			So(item, ShouldResemble, anotherValue)
   201  		})
   202  
   203  		Convey("ErrCantSatisfyMinTTL", func() {
   204  			_, err := testingCache.GetOrCreate(ctx, "item", func() ([]byte, time.Duration, error) {
   205  				return value, time.Minute, nil
   206  			}, WithMinTTL(2*time.Minute))
   207  			So(err, ShouldEqual, ErrCantSatisfyMinTTL)
   208  		})
   209  
   210  		oneRandomizedTrial := func(now, threshold time.Duration) (cacheHit bool) {
   211  			// Reset state (except RNG).
   212  			ctx := caching.WithEmptyProcessCache(ctx)
   213  			ctx, tc := testclock.UseTime(ctx, time.Date(2017, time.January, 1, 0, 0, 0, 0, time.UTC))
   214  
   215  			// Put the item in the cache.
   216  			item, err := testingCache.GetOrCreate(ctx, "item", func() ([]byte, time.Duration, error) {
   217  				return value, time.Hour, nil
   218  			})
   219  			So(err, ShouldBeNil)
   220  			So(item, ShouldResemble, value)
   221  
   222  			tc.Add(now)
   223  
   224  			// Grab it (or trigger a refresh if randomly expired).
   225  			item, err = testingCache.GetOrCreate(ctx, "item", func() ([]byte, time.Duration, error) {
   226  				return anotherValue, time.Hour, nil
   227  			}, WithRandomizedExpiration(threshold))
   228  			So(err, ShouldBeNil)
   229  			return bytes.Equal(item, value)
   230  		}
   231  
   232  		testCases := []struct {
   233  			now, threshold  time.Duration
   234  			expectedHitRate int
   235  		}{
   236  			{50 * time.Minute, 10 * time.Minute, 100}, // before threshold, no random expiration
   237  			{51 * time.Minute, 10 * time.Minute, 90},  // slightly above => some expiration
   238  			{59 * time.Minute, 10 * time.Minute, 11},  // almost at the threshold => heavy expiration
   239  			{61 * time.Minute, 10 * time.Minute, 0},   // outside item expiration => always expired
   240  		}
   241  
   242  		for i := 0; i < len(testCases); i++ {
   243  			now := testCases[i].now
   244  			threshold := testCases[i].threshold
   245  			expectedHitRate := testCases[i].expectedHitRate
   246  
   247  			Convey(fmt.Sprintf("WithRandomizedExpiration (now = %s)", now), func() {
   248  				cacheHits := 0
   249  				for i := 0; i < 100; i++ {
   250  					if oneRandomizedTrial(now, threshold) {
   251  						cacheHits++
   252  					}
   253  				}
   254  				So(cacheHits, ShouldEqual, expectedHitRate)
   255  			})
   256  		}
   257  	})
   258  }
   259  
   260  func TestSerialization(t *testing.T) {
   261  	t.Parallel()
   262  
   263  	Convey("With cache", t, func() {
   264  		now := time.Date(2017, time.January, 1, 0, 0, 0, 0, time.UTC)
   265  
   266  		c := Cache[[]byte]{
   267  			params: Parameters[[]byte]{
   268  				Marshal: func(item []byte) ([]byte, error) {
   269  					return item, nil
   270  				},
   271  				Unmarshal: func(blob []byte) ([]byte, error) {
   272  					return blob, nil
   273  				},
   274  			},
   275  		}
   276  
   277  		Convey("Happy path with deadline", func() {
   278  			originalItem := itemWithExp[[]byte]{[]byte("blah-blah"), now}
   279  
   280  			blob, err := c.serializeItem(&originalItem)
   281  			So(err, ShouldBeNil)
   282  
   283  			item, err := c.deserializeItem(blob)
   284  			So(err, ShouldBeNil)
   285  			So(item.exp.Equal(now), ShouldBeTrue)
   286  			So(item.val, ShouldResemble, originalItem.val)
   287  		})
   288  
   289  		Convey("Happy path without deadline", func() {
   290  			originalItem := itemWithExp[[]byte]{[]byte("blah-blah"), time.Time{}}
   291  
   292  			blob, err := c.serializeItem(&originalItem)
   293  			So(err, ShouldBeNil)
   294  
   295  			item, err := c.deserializeItem(blob)
   296  			So(err, ShouldBeNil)
   297  			So(item.exp.IsZero(), ShouldBeTrue)
   298  			So(item.val, ShouldResemble, originalItem.val)
   299  		})
   300  
   301  		Convey("Marshal error", func() {
   302  			fail := errors.New("failure")
   303  			c.params.Marshal = func(item []byte) ([]byte, error) {
   304  				return nil, fail
   305  			}
   306  			_, err := c.serializeItem(&itemWithExp[[]byte]{})
   307  			So(err, ShouldEqual, fail)
   308  		})
   309  
   310  		Convey("Small buffer in Unmarshal", func() {
   311  			_, err := c.deserializeItem([]byte{formatVersionByte, 0})
   312  			So(err, ShouldErrLike, "buffer is too small")
   313  		})
   314  
   315  		Convey("Bad version in Unmarshal", func() {
   316  			_, err := c.deserializeItem([]byte{formatVersionByte + 1, 0, 0, 0, 0, 0, 0, 0, 0})
   317  			So(err, ShouldErrLike, "bad format version")
   318  		})
   319  
   320  		Convey("Unmarshal error", func() {
   321  			fail := errors.New("failure")
   322  			c.params.Unmarshal = func(blob []byte) ([]byte, error) {
   323  				return nil, fail
   324  			}
   325  
   326  			blob, err := c.serializeItem(&itemWithExp[[]byte]{[]byte("blah-blah"), now})
   327  			So(err, ShouldBeNil)
   328  
   329  			_, err = c.deserializeItem(blob)
   330  			So(err, ShouldEqual, fail)
   331  		})
   332  	})
   333  }