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 }