OLD | NEW |
(Empty) | |
| 1 // Copyright 2015 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. |
| 4 |
| 5 package dscache |
| 6 |
| 7 import ( |
| 8 "bytes" |
| 9 "encoding/binary" |
| 10 "errors" |
| 11 "math/rand" |
| 12 "testing" |
| 13 "time" |
| 14 |
| 15 "github.com/luci/gae/filter/featureBreaker" |
| 16 "github.com/luci/gae/impl/memory" |
| 17 "github.com/luci/gae/service/datastore" |
| 18 "github.com/luci/gae/service/memcache" |
| 19 "github.com/luci/luci-go/common/clock" |
| 20 "github.com/luci/luci-go/common/clock/testclock" |
| 21 "github.com/luci/luci-go/common/mathrand" |
| 22 . "github.com/smartystreets/goconvey/convey" |
| 23 "golang.org/x/net/context" |
| 24 ) |
| 25 |
| 26 type object struct { |
| 27 ID int64 `gae:"$id"` |
| 28 |
| 29 Value string |
| 30 BigData []byte |
| 31 } |
| 32 |
| 33 type shardObj struct { // see shardsForKey() at top |
| 34 ID int64 `gae:"$id"` |
| 35 |
| 36 Value string |
| 37 } |
| 38 |
| 39 type noCacheObj struct { // see shardsForKey() at top |
| 40 ID string `gae:"$id"` |
| 41 |
| 42 Value bool |
| 43 } |
| 44 |
| 45 func init() { |
| 46 datastore.WritePropertyMapDeterministic = true |
| 47 |
| 48 internalValueSizeLimit = 2048 |
| 49 } |
| 50 |
| 51 func TestDSCache(t *testing.T) { |
| 52 t.Parallel() |
| 53 |
| 54 zeroTime, err := time.Parse("2006-01-02T15:04:05.999999999Z", "2006-01-0
2T15:04:05.999999999Z") |
| 55 if err != nil { |
| 56 panic(err) |
| 57 } |
| 58 |
| 59 Convey("Test dscache", t, func() { |
| 60 c := mathrand.Set(context.Background(), rand.New(rand.NewSource(
1))) |
| 61 clk := testclock.New(zeroTime) |
| 62 c = clock.Set(c, clk) |
| 63 c = memory.Use(c) |
| 64 |
| 65 dsUnder := datastore.Get(c) |
| 66 mc := memcache.Get(c) |
| 67 |
| 68 itmFor := func(i int, k datastore.Key) memcache.Item { |
| 69 return mc.NewItem(MakeMemcacheKey(i, k)) |
| 70 } |
| 71 |
| 72 shardsForKey := func(k datastore.Key) int { |
| 73 if k.Kind() == "shardObj" { |
| 74 return int(k.IntID()) |
| 75 } |
| 76 if k.Kind() == "noCacheObj" { |
| 77 return 0 |
| 78 } |
| 79 return DefaultShards |
| 80 } |
| 81 |
| 82 numMemcacheItems := func() uint64 { |
| 83 stats, err := mc.Stats() |
| 84 So(err, ShouldBeNil) |
| 85 return stats.Items |
| 86 } |
| 87 |
| 88 Convey("enabled cases", func() { |
| 89 c = FilterRDS(c, shardsForKey) |
| 90 ds := datastore.Get(c) |
| 91 So(dsUnder, ShouldNotBeNil) |
| 92 So(ds, ShouldNotBeNil) |
| 93 So(mc, ShouldNotBeNil) |
| 94 |
| 95 Convey("basically works", func() { |
| 96 pm := datastore.PropertyMap{ |
| 97 "BigData": {datastore.MkProperty([]byte(
""))}, |
| 98 "Value": {datastore.MkProperty("hi")}, |
| 99 } |
| 100 buf := &bytes.Buffer{} |
| 101 So(pm.Write(buf, datastore.WithoutContext), Shou
ldBeNil) |
| 102 encoded := append([]byte{0}, buf.Bytes()...) |
| 103 |
| 104 o := object{ID: 1, Value: "hi"} |
| 105 So(ds.Put(&o), ShouldBeNil) |
| 106 |
| 107 o = object{ID: 1} |
| 108 So(dsUnder.Get(&o), ShouldBeNil) |
| 109 So(o.Value, ShouldEqual, "hi") |
| 110 |
| 111 itm := itmFor(0, ds.KeyForObj(&o)) |
| 112 So(mc.Get(itm), ShouldEqual, memcache.ErrCacheMi
ss) |
| 113 |
| 114 o = object{ID: 1} |
| 115 So(ds.Get(&o), ShouldBeNil) |
| 116 So(o.Value, ShouldEqual, "hi") |
| 117 So(mc.Get(itm), ShouldBeNil) |
| 118 So(itm.Value(), ShouldResemble, encoded) |
| 119 |
| 120 Convey("now we don't need the datastore!", func(
) { |
| 121 o := object{ID: 1} |
| 122 |
| 123 // delete it, bypassing the cache filter
. Don't do this in production |
| 124 // unless you want a crappy cache. |
| 125 So(dsUnder.Delete(ds.KeyForObj(&o)), Sho
uldBeNil) |
| 126 |
| 127 itm := itmFor(0, ds.KeyForObj(&o)) |
| 128 So(mc.Get(itm), ShouldBeNil) |
| 129 So(itm.Value(), ShouldResemble, encoded) |
| 130 |
| 131 So(ds.Get(&o), ShouldBeNil) |
| 132 So(o.Value, ShouldEqual, "hi") |
| 133 }) |
| 134 |
| 135 Convey("deleting it properly records that fact,
however", func() { |
| 136 o := object{ID: 1} |
| 137 So(ds.Delete(ds.KeyForObj(&o)), ShouldBe
Nil) |
| 138 |
| 139 itm := itmFor(0, ds.KeyForObj(&o)) |
| 140 So(mc.Get(itm), ShouldEqual, memcache.Er
rCacheMiss) |
| 141 So(ds.Get(&o), ShouldEqual, datastore.Er
rNoSuchEntity) |
| 142 |
| 143 So(mc.Get(itm), ShouldBeNil) |
| 144 So(itm.Value(), ShouldResemble, []byte{}
) |
| 145 |
| 146 // this one hits memcache |
| 147 So(ds.Get(&o), ShouldEqual, datastore.Er
rNoSuchEntity) |
| 148 }) |
| 149 }) |
| 150 |
| 151 Convey("compression works", func() { |
| 152 o := object{ID: 2, Value: `¯\_(ツ)_/¯`} |
| 153 data := make([]byte, 4000) |
| 154 for i := range data { |
| 155 const alpha = "ABCDEFGHIJKLMNOPQRSTUVWXY
Zabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()" |
| 156 data[i] = alpha[i%len(alpha)] |
| 157 } |
| 158 o.BigData = data |
| 159 |
| 160 So(ds.Put(&o), ShouldBeNil) |
| 161 So(ds.Get(&o), ShouldBeNil) |
| 162 |
| 163 itm := itmFor(0, ds.KeyForObj(&o)) |
| 164 So(mc.Get(itm), ShouldBeNil) |
| 165 |
| 166 So(itm.Value()[0], ShouldEqual, ZlibCompression) |
| 167 So(len(itm.Value()), ShouldEqual, 653) // a bit
smaller than 4k |
| 168 |
| 169 // ensure the next Get comes from the cache |
| 170 So(dsUnder.Delete(ds.KeyForObj(&o)), ShouldBeNil
) |
| 171 |
| 172 o = object{ID: 2} |
| 173 So(ds.Get(&o), ShouldBeNil) |
| 174 So(o.Value, ShouldEqual, `¯\_(ツ)_/¯`) |
| 175 So(o.BigData, ShouldResemble, data) |
| 176 }) |
| 177 |
| 178 Convey("transactions", func() { |
| 179 Convey("work", func() { |
| 180 // populate an object @ ID1 |
| 181 So(ds.Put(&object{ID: 1, Value: "somethi
ng"}), ShouldBeNil) |
| 182 So(ds.Get(&object{ID: 1}), ShouldBeNil) |
| 183 |
| 184 So(ds.Put(&object{ID: 2, Value: "nurbs"}
), ShouldBeNil) |
| 185 So(ds.Get(&object{ID: 2}), ShouldBeNil) |
| 186 |
| 187 // memcache now has the wrong value (sim
ulated race) |
| 188 So(dsUnder.Put(&object{ID: 1, Value: "el
se"}), ShouldBeNil) |
| 189 So(ds.RunInTransaction(func(c context.Co
ntext) error { |
| 190 ds := datastore.Get(c) |
| 191 o := &object{ID: 1} |
| 192 So(ds.Get(o), ShouldBeNil) |
| 193 So(o.Value, ShouldEqual, "else") |
| 194 o.Value = "txn" |
| 195 So(ds.Put(o), ShouldBeNil) |
| 196 |
| 197 So(ds.Delete(ds.KeyForObj(&objec
t{ID: 2})), ShouldBeNil) |
| 198 return nil |
| 199 }, &datastore.TransactionOptions{XG: tru
e}), ShouldBeNil) |
| 200 |
| 201 So(mc.Get(itmFor(0, ds.KeyForObj(&object
{ID: 1}))), |
| 202 ShouldEqual, memcache.ErrCacheMi
ss) |
| 203 So(mc.Get(itmFor(0, ds.KeyForObj(&object
{ID: 2}))), |
| 204 ShouldEqual, memcache.ErrCacheMi
ss) |
| 205 o := &object{ID: 1} |
| 206 So(ds.Get(o), ShouldBeNil) |
| 207 So(o.Value, ShouldEqual, "txn") |
| 208 }) |
| 209 |
| 210 Convey("errors don't invalidate", func() { |
| 211 // populate an object @ ID1 |
| 212 So(ds.Put(&object{ID: 1, Value: "somethi
ng"}), ShouldBeNil) |
| 213 So(ds.Get(&object{ID: 1}), ShouldBeNil) |
| 214 So(numMemcacheItems(), ShouldEqual, 1) |
| 215 |
| 216 So(ds.RunInTransaction(func(c context.Co
ntext) error { |
| 217 ds := datastore.Get(c) |
| 218 o := &object{ID: 1} |
| 219 So(ds.Get(o), ShouldBeNil) |
| 220 So(o.Value, ShouldEqual, "someth
ing") |
| 221 o.Value = "txn" |
| 222 So(ds.Put(o), ShouldBeNil) |
| 223 return errors.New("OH NOES") |
| 224 }, nil).Error(), ShouldContainSubstring,
"OH NOES") |
| 225 |
| 226 // memcache still has the original |
| 227 So(numMemcacheItems(), ShouldEqual, 1) |
| 228 So(dsUnder.Delete(ds.KeyForObj(&object{I
D: 1})), ShouldBeNil) |
| 229 o := &object{ID: 1} |
| 230 So(ds.Get(o), ShouldBeNil) |
| 231 So(o.Value, ShouldEqual, "something") |
| 232 }) |
| 233 }) |
| 234 |
| 235 Convey("control", func() { |
| 236 Convey("per-model bypass", func() { |
| 237 type model struct { |
| 238 ID string `gae
:"$id"` |
| 239 UseDSCache datastore.Toggle `gae
:"$dscache.enable,false"` |
| 240 |
| 241 Value string |
| 242 } |
| 243 |
| 244 itms := []model{ |
| 245 {ID: "hi", Value: "something"}, |
| 246 {ID: "there", Value: "else", Use
DSCache: datastore.On}, |
| 247 } |
| 248 |
| 249 So(ds.PutMulti(itms), ShouldBeNil) |
| 250 So(ds.GetMulti(itms), ShouldBeNil) |
| 251 |
| 252 So(numMemcacheItems(), ShouldEqual, 1) |
| 253 }) |
| 254 |
| 255 Convey("per-key shard count", func() { |
| 256 s := &shardObj{ID: 4, Value: "hi"} |
| 257 So(ds.Put(s), ShouldBeNil) |
| 258 So(ds.Get(s), ShouldBeNil) |
| 259 |
| 260 So(numMemcacheItems(), ShouldEqual, 1) |
| 261 for i := 0; i < 20; i++ { |
| 262 So(ds.Get(s), ShouldBeNil) |
| 263 } |
| 264 So(numMemcacheItems(), ShouldEqual, 4) |
| 265 }) |
| 266 |
| 267 Convey("per-key cache disablement", func() { |
| 268 n := &noCacheObj{ID: "nurbs", Value: tru
e} |
| 269 So(ds.Put(n), ShouldBeNil) |
| 270 So(ds.Get(n), ShouldBeNil) |
| 271 So(numMemcacheItems(), ShouldEqual, 0) |
| 272 }) |
| 273 |
| 274 Convey("per-model expiration", func() { |
| 275 type model struct { |
| 276 ID int64 `gae:"$id"` |
| 277 DSCacheExp int64 `gae:"$dscache.
expiration,7"` |
| 278 |
| 279 Value string |
| 280 } |
| 281 |
| 282 So(ds.Put(&model{ID: 1, Value: "mooo"}),
ShouldBeNil) |
| 283 So(ds.Get(&model{ID: 1}), ShouldBeNil) |
| 284 |
| 285 itm := itmFor(0, ds.KeyForObj(&model{ID:
1})) |
| 286 So(mc.Get(itm), ShouldBeNil) |
| 287 |
| 288 clk.Add(10 * time.Second) |
| 289 So(mc.Get(itm), ShouldEqual, memcache.Er
rCacheMiss) |
| 290 }) |
| 291 }) |
| 292 |
| 293 Convey("screw cases", func() { |
| 294 Convey("memcache contains bogus value (simulated
failed AddMulti)", func() { |
| 295 o := &object{ID: 1, Value: "spleen"} |
| 296 So(ds.Put(o), ShouldBeNil) |
| 297 |
| 298 sekret := []byte("I am a banana") |
| 299 itm := itmFor(0, ds.KeyForObj(o)).SetVal
ue(sekret) |
| 300 So(mc.Set(itm), ShouldBeNil) |
| 301 |
| 302 o = &object{ID: 1} |
| 303 So(ds.Get(o), ShouldBeNil) |
| 304 So(o.Value, ShouldEqual, "spleen") |
| 305 |
| 306 So(mc.Get(itm), ShouldBeNil) |
| 307 So(itm.Flags(), ShouldEqual, ItemUKNONWN
) |
| 308 So(itm.Value(), ShouldResemble, sekret) |
| 309 }) |
| 310 |
| 311 Convey("memcache contains bogus value (corrupt e
ntry)", func() { |
| 312 o := &object{ID: 1, Value: "spleen"} |
| 313 So(ds.Put(o), ShouldBeNil) |
| 314 |
| 315 sekret := []byte("I am a banana") |
| 316 itm := (itmFor(0, ds.KeyForObj(o)). |
| 317 SetValue(sekret). |
| 318 SetFlags(uint32(ItemHasData))) |
| 319 So(mc.Set(itm), ShouldBeNil) |
| 320 |
| 321 o = &object{ID: 1} |
| 322 So(ds.Get(o), ShouldBeNil) |
| 323 So(o.Value, ShouldEqual, "spleen") |
| 324 |
| 325 So(mc.Get(itm), ShouldBeNil) |
| 326 So(itm.Flags(), ShouldEqual, ItemHasData
) |
| 327 So(itm.Value(), ShouldResemble, sekret) |
| 328 }) |
| 329 |
| 330 Convey("other entity has the lock", func() { |
| 331 o := &object{ID: 1, Value: "spleen"} |
| 332 So(ds.Put(o), ShouldBeNil) |
| 333 |
| 334 sekret := []byte("r@vmarod!#)%9T") |
| 335 itm := (itmFor(0, ds.KeyForObj(o)). |
| 336 SetValue(sekret). |
| 337 SetFlags(uint32(ItemHasLock))) |
| 338 So(mc.Set(itm), ShouldBeNil) |
| 339 |
| 340 o = &object{ID: 1} |
| 341 So(ds.Get(o), ShouldBeNil) |
| 342 So(o.Value, ShouldEqual, "spleen") |
| 343 |
| 344 So(mc.Get(itm), ShouldBeNil) |
| 345 So(itm.Flags(), ShouldEqual, ItemHasLock
) |
| 346 So(itm.Value(), ShouldResemble, sekret) |
| 347 }) |
| 348 |
| 349 Convey("massive entities can't be cached", func(
) { |
| 350 o := &object{ID: 1, Value: "spleen"} |
| 351 mr := mathrand.Get(c) |
| 352 numRounds := (internalValueSizeLimit / 8
) * 2 |
| 353 buf := bytes.Buffer{} |
| 354 for i := 0; i < numRounds; i++ { |
| 355 So(binary.Write(&buf, binary.Lit
tleEndian, mr.Int63()), ShouldBeNil) |
| 356 } |
| 357 o.BigData = buf.Bytes() |
| 358 So(ds.Put(o), ShouldBeNil) |
| 359 |
| 360 o.BigData = nil |
| 361 So(ds.Get(o), ShouldBeNil) |
| 362 |
| 363 itm := itmFor(0, ds.KeyForObj(o)) |
| 364 So(mc.Get(itm), ShouldBeNil) |
| 365 |
| 366 // Is locked until the next put, forcing
all access to the datastore. |
| 367 So(itm.Value(), ShouldResemble, []byte{}
) |
| 368 So(itm.Flags(), ShouldEqual, ItemHasLock
) |
| 369 |
| 370 o.BigData = []byte("hi :)") |
| 371 So(ds.Put(o), ShouldBeNil) |
| 372 So(ds.Get(o), ShouldBeNil) |
| 373 |
| 374 So(mc.Get(itm), ShouldBeNil) |
| 375 So(itm.Flags(), ShouldEqual, ItemHasData
) |
| 376 }) |
| 377 |
| 378 Convey("failure on Setting memcache locks is a h
ard stop", func() { |
| 379 c, fb := featureBreaker.FilterMC(c, nil) |
| 380 fb.BreakFeatures(nil, "SetMulti") |
| 381 ds := datastore.Get(c) |
| 382 So(ds.Put(&object{ID: 1}).Error(), Shoul
dContainSubstring, "SetMulti") |
| 383 }) |
| 384 |
| 385 Convey("failure on Setting memcache locks in a t
ransaction is a hard stop", func() { |
| 386 c, fb := featureBreaker.FilterMC(c, nil) |
| 387 fb.BreakFeatures(nil, "SetMulti") |
| 388 ds := datastore.Get(c) |
| 389 So(ds.RunInTransaction(func(c context.Co
ntext) error { |
| 390 So(datastore.Get(c).Put(&object{
ID: 1}), ShouldBeNil) |
| 391 // no problems here... memcache
operations happen after the function |
| 392 // body quits. |
| 393 return nil |
| 394 }, nil).Error(), ShouldContainSubstring,
"SetMulti") |
| 395 }) |
| 396 |
| 397 }) |
| 398 |
| 399 Convey("misc", func() { |
| 400 Convey("verify numShards caps at MaxShards", fun
c() { |
| 401 sc := supportContext{shardsForKey: shard
sForKey} |
| 402 So(sc.numShards(ds.KeyForObj(&shardObj{I
D: 9001})), ShouldEqual, MaxShards) |
| 403 }) |
| 404 |
| 405 Convey("CompressionType.String", func() { |
| 406 So(NoCompression.String(), ShouldEqual,
"NoCompression") |
| 407 So(ZlibCompression.String(), ShouldEqual
, "ZlibCompression") |
| 408 So(CompressionType(100).String(), Should
Equal, "UNKNOWN_CompressionType(100)") |
| 409 }) |
| 410 }) |
| 411 }) |
| 412 |
| 413 Convey("disabled cases", func() { |
| 414 defer func() { |
| 415 globalEnabled = true |
| 416 }() |
| 417 |
| 418 So(IsGloballyEnabled(c), ShouldBeTrue) |
| 419 |
| 420 So(SetDynamicGlobalEnable(c, false), ShouldBeNil) |
| 421 // twice is a nop |
| 422 So(SetDynamicGlobalEnable(c, false), ShouldBeNil) |
| 423 |
| 424 // but it takes 5 minutes to kick in |
| 425 So(IsGloballyEnabled(c), ShouldBeTrue) |
| 426 clk.Add(time.Minute*5 + time.Second) |
| 427 So(IsGloballyEnabled(c), ShouldBeFalse) |
| 428 |
| 429 So(mc.Set(mc.NewItem("test").SetValue([]byte("hi"))), Sh
ouldBeNil) |
| 430 So(numMemcacheItems(), ShouldEqual, 1) |
| 431 So(SetDynamicGlobalEnable(c, true), ShouldBeNil) |
| 432 // memcache gets flushed as a side effect |
| 433 So(numMemcacheItems(), ShouldEqual, 0) |
| 434 |
| 435 // Still takes 5 minutes to kick in |
| 436 So(IsGloballyEnabled(c), ShouldBeFalse) |
| 437 clk.Add(time.Minute*5 + time.Second) |
| 438 So(IsGloballyEnabled(c), ShouldBeTrue) |
| 439 }) |
| 440 }) |
| 441 } |
| 442 |
| 443 func TestStaticEnable(t *testing.T) { |
| 444 // intentionally not parallel b/c deals with global variable |
| 445 // t.Parallel() |
| 446 |
| 447 Convey("Test InstanceEnabledStatic", t, func() { |
| 448 InstanceEnabledStatic = false |
| 449 defer func() { |
| 450 InstanceEnabledStatic = true |
| 451 }() |
| 452 |
| 453 c := context.Background() |
| 454 newC := FilterRDS(c, nil) |
| 455 So(newC, ShouldEqual, c) |
| 456 }) |
| 457 } |
OLD | NEW |