go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/filter/dscache/dscache_test.go (about) 1 // Copyright 2015 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 dscache 16 17 import ( 18 "bytes" 19 "context" 20 "encoding/binary" 21 "errors" 22 "math/rand" 23 "testing" 24 "time" 25 26 "go.chromium.org/luci/common/clock" 27 "go.chromium.org/luci/common/clock/testclock" 28 "go.chromium.org/luci/common/data/rand/mathrand" 29 30 "go.chromium.org/luci/gae/filter/featureBreaker" 31 "go.chromium.org/luci/gae/impl/memory" 32 ds "go.chromium.org/luci/gae/service/datastore" 33 mc "go.chromium.org/luci/gae/service/memcache" 34 35 . "github.com/smartystreets/goconvey/convey" 36 ) 37 38 type object struct { 39 ID int64 `gae:"$id"` 40 41 Value string 42 BigData []byte 43 44 Nested nested `gae:",lsp"` 45 } 46 47 type nested struct { 48 Kind string `gae:"$kind,NestedKind"` 49 ID string `gae:"$id"` 50 Value int64 51 } 52 53 type shardObj struct { 54 ID int64 `gae:"$id"` 55 56 Value string 57 } 58 59 func shardObjFn(k *ds.Key) (amt int, ok bool) { 60 if last := k.LastTok(); last.Kind == "shardObj" { 61 amt = int(last.IntID) 62 ok = true 63 } 64 return 65 } 66 67 type noCacheObj struct { 68 ID string `gae:"$id"` 69 70 Value bool 71 } 72 73 func noCacheObjFn(k *ds.Key) (amt int, ok bool) { 74 if k.Kind() == "noCacheObj" { 75 ok = true 76 } 77 return 78 } 79 80 func init() { 81 ds.WritePropertyMapDeterministic = true 82 83 internalValueSizeLimit = CompressionThreshold + 2048 84 } 85 86 func TestDSCache(t *testing.T) { 87 t.Parallel() 88 89 zeroTime, err := time.Parse("2006-01-02T15:04:05.999999999Z", "2006-01-02T15:04:05.999999999Z") 90 if err != nil { 91 panic(err) 92 } 93 94 Convey("Test dscache", t, func() { 95 c := mathrand.Set(context.Background(), rand.New(rand.NewSource(1))) 96 clk := testclock.New(zeroTime) 97 c = clock.Set(c, clk) 98 c = memory.Use(c) 99 100 underCtx := c 101 102 numMemcacheItems := func() uint64 { 103 stats, err := mc.Stats(c) 104 So(err, ShouldBeNil) 105 return stats.Items 106 } 107 108 c = FilterRDS(c, nil) 109 c = AddShardFunctions(c, shardObjFn, noCacheObjFn) 110 111 Convey("basically works", func() { 112 pm := ds.PropertyMap{ 113 "BigData": ds.MkProperty([]byte("")), 114 "Value": ds.MkProperty("hi"), 115 "Nested": ds.MkProperty(ds.PropertyMap{ 116 "$key": ds.MkProperty(ds.NewKey(c, "NestedKind", "ho", 0, nil)), 117 "Value": ds.MkProperty(123), 118 }), 119 } 120 encoded := append([]byte{0}, ds.Serialize.ToBytes(pm)...) 121 122 o := object{ID: 1, Value: "hi", Nested: nested{ID: "ho", Value: 123}} 123 So(ds.Put(c, &o), ShouldBeNil) 124 125 expected := o 126 expected.Nested.Kind = "NestedKind" 127 128 o = object{ID: 1} 129 So(ds.Get(underCtx, &o), ShouldBeNil) 130 So(o, ShouldResemble, expected) 131 132 itm, err := mc.GetKey(c, makeMemcacheKey(0, ds.KeyForObj(c, &o))) 133 So(err, ShouldEqual, mc.ErrCacheMiss) 134 135 o = object{ID: 1} 136 So(ds.Get(c, &o), ShouldBeNil) 137 So(o, ShouldResemble, expected) 138 139 itm, err = mc.GetKey(c, itm.Key()) 140 So(err, ShouldBeNil) 141 So(itm.Value(), ShouldResemble, encoded) 142 143 Convey("now we don't need the datastore!", func() { 144 o := object{ID: 1} 145 146 // delete it, bypassing the cache filter. Don't do this in production 147 // unless you want a crappy cache. 148 So(ds.Delete(underCtx, ds.KeyForObj(underCtx, &o)), ShouldBeNil) 149 150 itm, err := mc.GetKey(c, makeMemcacheKey(0, ds.KeyForObj(c, &o))) 151 So(err, ShouldBeNil) 152 So(itm.Value(), ShouldResemble, encoded) 153 154 So(ds.Get(c, &o), ShouldBeNil) 155 So(o, ShouldResemble, expected) 156 }) 157 158 Convey("deleting it properly records that fact, however", func() { 159 o := object{ID: 1} 160 So(ds.Delete(c, ds.KeyForObj(c, &o)), ShouldBeNil) 161 162 itm, err := mc.GetKey(c, makeMemcacheKey(0, ds.KeyForObj(c, &o))) 163 So(err, ShouldEqual, mc.ErrCacheMiss) 164 So(ds.Get(c, &o), ShouldEqual, ds.ErrNoSuchEntity) 165 166 itm, err = mc.GetKey(c, itm.Key()) 167 So(err, ShouldBeNil) 168 So(itm.Value(), ShouldResemble, []byte{}) 169 170 // this one hits memcache 171 So(ds.Get(c, &o), ShouldEqual, ds.ErrNoSuchEntity) 172 }) 173 }) 174 175 Convey("compression works", func() { 176 o := object{ID: 2, Value: `¯\_(ツ)_/¯`} 177 data := make([]byte, CompressionThreshold+1) 178 for i := range data { 179 const alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()" 180 data[i] = alpha[i%len(alpha)] 181 } 182 o.BigData = data 183 184 So(ds.Put(c, &o), ShouldBeNil) 185 So(ds.Get(c, &o), ShouldBeNil) 186 187 itm, err := mc.GetKey(c, makeMemcacheKey(0, ds.KeyForObj(c, &o))) 188 So(err, ShouldBeNil) 189 190 algo := compressionZlib 191 if UseZstd { 192 algo = compressionZstd 193 } 194 So(itm.Value()[0], ShouldEqual, algo) 195 So(len(itm.Value()), ShouldBeLessThan, len(data)) 196 197 Convey("uses compressed cache entry", func() { 198 // ensure the next Get comes from the cache 199 So(ds.Delete(underCtx, ds.KeyForObj(underCtx, &o)), ShouldBeNil) 200 201 o = object{ID: 2} 202 So(ds.Get(c, &o), ShouldBeNil) 203 So(o.Value, ShouldEqual, `¯\_(ツ)_/¯`) 204 So(o.BigData, ShouldResemble, data) 205 }) 206 207 Convey("skips unknown compression algo", func() { 208 blob := append([]byte(nil), itm.Value()...) 209 blob[0] = 123 // unknown algo 210 itm.SetValue(blob) 211 212 So(mc.Set(c, itm), ShouldBeNil) 213 214 // Should fallback to fetching from datastore. 215 o = object{ID: 2} 216 So(ds.Get(c, &o), ShouldBeNil) 217 So(o.Value, ShouldEqual, `¯\_(ツ)_/¯`) 218 So(o.BigData, ShouldResemble, data) 219 }) 220 }) 221 222 Convey("transactions", func() { 223 Convey("work", func() { 224 // populate an object @ ID1 225 So(ds.Put(c, &object{ID: 1, Value: "something"}), ShouldBeNil) 226 So(ds.Get(c, &object{ID: 1}), ShouldBeNil) 227 228 So(ds.Put(c, &object{ID: 2, Value: "nurbs"}), ShouldBeNil) 229 So(ds.Get(c, &object{ID: 2}), ShouldBeNil) 230 231 // memcache now has the wrong value (simulated race) 232 So(ds.Put(underCtx, &object{ID: 1, Value: "else"}), ShouldBeNil) 233 So(ds.RunInTransaction(c, func(c context.Context) error { 234 o := &object{ID: 1} 235 So(ds.Get(c, o), ShouldBeNil) 236 So(o.Value, ShouldEqual, "else") 237 o.Value = "txn" 238 So(ds.Put(c, o), ShouldBeNil) 239 240 So(ds.Delete(c, ds.KeyForObj(c, &object{ID: 2})), ShouldBeNil) 241 return nil 242 }, nil), ShouldBeNil) 243 244 _, err := mc.GetKey(c, makeMemcacheKey(0, ds.KeyForObj(c, &object{ID: 1}))) 245 So(err, ShouldEqual, mc.ErrCacheMiss) 246 _, err = mc.GetKey(c, makeMemcacheKey(0, ds.KeyForObj(c, &object{ID: 2}))) 247 So(err, ShouldEqual, mc.ErrCacheMiss) 248 o := &object{ID: 1} 249 So(ds.Get(c, o), ShouldBeNil) 250 So(o.Value, ShouldEqual, "txn") 251 }) 252 253 Convey("errors don't invalidate", func() { 254 // populate an object @ ID1 255 So(ds.Put(c, &object{ID: 1, Value: "something"}), ShouldBeNil) 256 So(ds.Get(c, &object{ID: 1}), ShouldBeNil) 257 So(numMemcacheItems(), ShouldEqual, 1) 258 259 So(ds.RunInTransaction(c, func(c context.Context) error { 260 o := &object{ID: 1} 261 So(ds.Get(c, o), ShouldBeNil) 262 So(o.Value, ShouldEqual, "something") 263 o.Value = "txn" 264 So(ds.Put(c, o), ShouldBeNil) 265 return errors.New("OH NOES") 266 }, nil).Error(), ShouldContainSubstring, "OH NOES") 267 268 // memcache still has the original 269 So(numMemcacheItems(), ShouldEqual, 1) 270 So(ds.Delete(underCtx, ds.KeyForObj(underCtx, &object{ID: 1})), ShouldBeNil) 271 o := &object{ID: 1} 272 So(ds.Get(c, o), ShouldBeNil) 273 So(o.Value, ShouldEqual, "something") 274 }) 275 }) 276 277 Convey("control", func() { 278 Convey("per-model bypass", func() { 279 type model struct { 280 ID string `gae:"$id"` 281 UseDSCache ds.Toggle `gae:"$dscache.enable,false"` 282 283 Value string 284 } 285 286 itms := []model{ 287 {ID: "hi", Value: "something"}, 288 {ID: "there", Value: "else", UseDSCache: ds.On}, 289 } 290 291 So(ds.Put(c, itms), ShouldBeNil) 292 So(ds.Get(c, itms), ShouldBeNil) 293 294 So(numMemcacheItems(), ShouldEqual, 1) 295 }) 296 297 Convey("per-key shard count", func() { 298 s := &shardObj{ID: 4, Value: "hi"} 299 So(ds.Put(c, s), ShouldBeNil) 300 So(ds.Get(c, s), ShouldBeNil) 301 302 So(numMemcacheItems(), ShouldEqual, 1) 303 for i := 0; i < 20; i++ { 304 So(ds.Get(c, s), ShouldBeNil) 305 } 306 So(numMemcacheItems(), ShouldEqual, 4) 307 }) 308 309 Convey("per-key cache disablement", func() { 310 n := &noCacheObj{ID: "nurbs", Value: true} 311 So(ds.Put(c, n), ShouldBeNil) 312 So(ds.Get(c, n), ShouldBeNil) 313 So(numMemcacheItems(), ShouldEqual, 0) 314 }) 315 316 Convey("per-model expiration", func() { 317 type model struct { 318 ID int64 `gae:"$id"` 319 DSCacheExp int64 `gae:"$dscache.expiration,7"` 320 321 Value string 322 } 323 324 So(ds.Put(c, &model{ID: 1, Value: "mooo"}), ShouldBeNil) 325 So(ds.Get(c, &model{ID: 1}), ShouldBeNil) 326 327 itm, err := mc.GetKey(c, makeMemcacheKey(0, ds.KeyForObj(c, &model{ID: 1}))) 328 So(err, ShouldBeNil) 329 330 clk.Add(10 * time.Second) 331 _, err = mc.GetKey(c, itm.Key()) 332 So(err, ShouldEqual, mc.ErrCacheMiss) 333 }) 334 }) 335 336 Convey("screw cases", func() { 337 Convey("memcache contains bogus value (simulated failed AddMulti)", func() { 338 o := &object{ID: 1, Value: "spleen"} 339 So(ds.Put(c, o), ShouldBeNil) 340 341 sekret := []byte("I am a banana") 342 itm := mc.NewItem(c, makeMemcacheKey(0, ds.KeyForObj(c, o))).SetValue(sekret) 343 So(mc.Set(c, itm), ShouldBeNil) 344 345 o = &object{ID: 1} 346 So(ds.Get(c, o), ShouldBeNil) 347 So(o.Value, ShouldEqual, "spleen") 348 349 itm, err := mc.GetKey(c, itm.Key()) 350 So(err, ShouldBeNil) 351 So(itm.Flags(), ShouldEqual, itemFlagUnknown) 352 So(itm.Value(), ShouldResemble, sekret) 353 }) 354 355 Convey("memcache contains bogus value (corrupt entry)", func() { 356 o := &object{ID: 1, Value: "spleen"} 357 So(ds.Put(c, o), ShouldBeNil) 358 359 sekret := []byte("I am a banana") 360 itm := (mc.NewItem(c, makeMemcacheKey(0, ds.KeyForObj(c, o))). 361 SetValue(sekret). 362 SetFlags(itemFlagHasData)) 363 So(mc.Set(c, itm), ShouldBeNil) 364 365 o = &object{ID: 1} 366 So(ds.Get(c, o), ShouldBeNil) 367 So(o.Value, ShouldEqual, "spleen") 368 369 itm, err := mc.GetKey(c, itm.Key()) 370 So(err, ShouldBeNil) 371 So(itm.Flags(), ShouldEqual, itemFlagHasData) 372 So(itm.Value(), ShouldResemble, sekret) 373 }) 374 375 Convey("other entity has the lock", func() { 376 o := &object{ID: 1, Value: "spleen"} 377 So(ds.Put(c, o), ShouldBeNil) 378 379 sekret := []byte("r@vmarod!#)%9T") 380 itm := (mc.NewItem(c, makeMemcacheKey(0, ds.KeyForObj(c, o))). 381 SetValue(sekret). 382 SetFlags(itemFlagHasLock)) 383 So(mc.Set(c, itm), ShouldBeNil) 384 385 o = &object{ID: 1} 386 So(ds.Get(c, o), ShouldBeNil) 387 So(o.Value, ShouldEqual, "spleen") 388 389 itm, err := mc.GetKey(c, itm.Key()) 390 So(err, ShouldBeNil) 391 So(itm.Flags(), ShouldEqual, itemFlagHasLock) 392 So(itm.Value(), ShouldResemble, sekret) 393 }) 394 395 Convey("massive entities can't be cached", func() { 396 o := &object{ID: 1, Value: "spleen"} 397 mr := mathrand.Get(c) 398 numRounds := (internalValueSizeLimit / 8) * 2 399 buf := bytes.Buffer{} 400 for i := 0; i < numRounds; i++ { 401 So(binary.Write(&buf, binary.LittleEndian, mr.Int63()), ShouldBeNil) 402 } 403 o.BigData = buf.Bytes() 404 So(ds.Put(c, o), ShouldBeNil) 405 406 o.BigData = nil 407 So(ds.Get(c, o), ShouldBeNil) 408 409 itm, err := mc.GetKey(c, makeMemcacheKey(0, ds.KeyForObj(c, o))) 410 So(err, ShouldBeNil) 411 412 // Is locked until the next put, forcing all access to the datastore. 413 So(itm.Value(), ShouldResemble, []byte{}) 414 So(itm.Flags(), ShouldEqual, itemFlagHasLock) 415 416 o.BigData = []byte("hi :)") 417 So(ds.Put(c, o), ShouldBeNil) 418 So(ds.Get(c, o), ShouldBeNil) 419 420 itm, err = mc.GetKey(c, itm.Key()) 421 So(err, ShouldBeNil) 422 So(itm.Flags(), ShouldEqual, itemFlagHasData) 423 }) 424 425 Convey("failure on Setting memcache locks is a hard stop", func() { 426 c, fb := featureBreaker.FilterMC(c, nil) 427 fb.BreakFeatures(nil, "SetMulti") 428 So(ds.Put(c, &object{ID: 1}).Error(), ShouldContainSubstring, "SetMulti") 429 }) 430 431 Convey("failure on Setting memcache locks in a transaction is a hard stop", func() { 432 c, fb := featureBreaker.FilterMC(c, nil) 433 fb.BreakFeatures(nil, "SetMulti") 434 So(ds.RunInTransaction(c, func(c context.Context) error { 435 So(ds.Put(c, &object{ID: 1}), ShouldBeNil) 436 // no problems here... memcache operations happen after the function 437 // body quits. 438 return nil 439 }, nil).Error(), ShouldContainSubstring, "SetMulti") 440 }) 441 442 Convey("verify numShards caps at MaxShards", func() { 443 sc := supportContext{shardsForKey: []ShardFunction{shardObjFn}} 444 So(sc.numShards(ds.KeyForObj(c, &shardObj{ID: 9001})), ShouldEqual, MaxShards) 445 }) 446 }) 447 }) 448 }