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/impl/memory" |
| 16 "github.com/luci/gae/service/datastore" |
| 17 "github.com/luci/gae/service/memcache" |
| 18 "github.com/luci/luci-go/common/clock" |
| 19 "github.com/luci/luci-go/common/clock/testclock" |
| 20 "github.com/luci/luci-go/common/mathrand" |
| 21 . "github.com/smartystreets/goconvey/convey" |
| 22 "golang.org/x/net/context" |
| 23 ) |
| 24 |
| 25 type object struct { |
| 26 ID int64 `gae:"$id"` |
| 27 |
| 28 Value string |
| 29 BigData []byte |
| 30 } |
| 31 |
| 32 type shardObj struct { // see shardsForKey() at top |
| 33 ID int64 `gae:"$id"` |
| 34 |
| 35 Value string |
| 36 } |
| 37 |
| 38 type noCacheObj struct { // see shardsForKey() at top |
| 39 ID string `gae:"$id"` |
| 40 |
| 41 Value bool |
| 42 } |
| 43 |
| 44 func init() { |
| 45 datastore.WritePropertyMapDeterministic = true |
| 46 |
| 47 internalValueSizeLimit = 2048 |
| 48 } |
| 49 |
| 50 func TestDSCache(t *testing.T) { |
| 51 t.Parallel() |
| 52 |
| 53 zeroTime, err := time.Parse("2006-01-02T15:04:05.999999999Z", "2006-01-0
2T15:04:05.999999999Z") |
| 54 if err != nil { |
| 55 panic(err) |
| 56 } |
| 57 |
| 58 Convey("Test dscache", t, func() { |
| 59 c := mathrand.Set(context.Background(), rand.New(rand.NewSource(
1))) |
| 60 clk := testclock.New(zeroTime) |
| 61 c = clock.Set(c, clk) |
| 62 c = memory.Use(c) |
| 63 dsUnder := datastore.Get(c) |
| 64 |
| 65 shardsForKey := func(k datastore.Key) int { |
| 66 if k.Kind() == "shardObj" { |
| 67 return int(k.IntID()) |
| 68 } |
| 69 if k.Kind() == "noCacheObj" { |
| 70 return 0 |
| 71 } |
| 72 return DefaultShards |
| 73 } |
| 74 |
| 75 ds := datastore.Get(FilterRDS(c, shardsForKey)) |
| 76 mc := memcache.Get(c) |
| 77 |
| 78 itmFor := func(i int, k datastore.Key) memcache.Item { |
| 79 return mc.NewItem(MakeMemcacheKey(i, k)) |
| 80 } |
| 81 |
| 82 numMemcacheItems := func() uint64 { |
| 83 stats, err := mc.Stats() |
| 84 So(err, ShouldBeNil) |
| 85 return stats.Items |
| 86 } |
| 87 |
| 88 So(dsUnder, ShouldNotBeNil) |
| 89 So(ds, ShouldNotBeNil) |
| 90 So(mc, ShouldNotBeNil) |
| 91 |
| 92 Convey("basically works", func() { |
| 93 pm := datastore.PropertyMap{ |
| 94 "BigData": {datastore.MkProperty([]byte(""))}, |
| 95 "Value": {datastore.MkProperty("hi")}, |
| 96 } |
| 97 buf := &bytes.Buffer{} |
| 98 So(pm.Write(buf, datastore.WithoutContext), ShouldBeNil) |
| 99 encoded := append([]byte{0}, buf.Bytes()...) |
| 100 |
| 101 o := object{ID: 1, Value: "hi"} |
| 102 So(ds.Put(&o), ShouldBeNil) |
| 103 |
| 104 o = object{ID: 1} |
| 105 So(dsUnder.Get(&o), ShouldBeNil) |
| 106 So(o.Value, ShouldEqual, "hi") |
| 107 |
| 108 itm := itmFor(0, ds.KeyForObj(&o)) |
| 109 So(mc.Get(itm), ShouldEqual, memcache.ErrCacheMiss) |
| 110 |
| 111 o = object{ID: 1} |
| 112 So(ds.Get(&o), ShouldBeNil) |
| 113 So(o.Value, ShouldEqual, "hi") |
| 114 So(mc.Get(itm), ShouldBeNil) |
| 115 So(itm.Value(), ShouldResemble, encoded) |
| 116 |
| 117 Convey("now we don't need the datastore!", func() { |
| 118 o := object{ID: 1} |
| 119 |
| 120 // delete it, bypassing the cache filter. Don't
do this in production |
| 121 // unless you want a crappy cache. |
| 122 So(dsUnder.Delete(ds.KeyForObj(&o)), ShouldBeNil
) |
| 123 |
| 124 itm := itmFor(0, ds.KeyForObj(&o)) |
| 125 So(mc.Get(itm), ShouldBeNil) |
| 126 So(itm.Value(), ShouldResemble, encoded) |
| 127 |
| 128 So(ds.Get(&o), ShouldBeNil) |
| 129 So(o.Value, ShouldEqual, "hi") |
| 130 }) |
| 131 |
| 132 Convey("deleting it properly records that fact, however"
, func() { |
| 133 o := object{ID: 1} |
| 134 So(ds.Delete(ds.KeyForObj(&o)), ShouldBeNil) |
| 135 |
| 136 itm := itmFor(0, ds.KeyForObj(&o)) |
| 137 So(mc.Get(itm), ShouldEqual, memcache.ErrCacheMi
ss) |
| 138 So(ds.Get(&o), ShouldEqual, datastore.ErrNoSuchE
ntity) |
| 139 |
| 140 So(mc.Get(itm), ShouldBeNil) |
| 141 So(itm.Value(), ShouldResemble, []byte{}) |
| 142 |
| 143 // this one hits memcache |
| 144 So(ds.Get(&o), ShouldEqual, datastore.ErrNoSuchE
ntity) |
| 145 }) |
| 146 }) |
| 147 |
| 148 Convey("compression works", func() { |
| 149 o := object{ID: 2, Value: `¯\_(ツ)_/¯`} |
| 150 data := make([]byte, 4000) |
| 151 for i := range data { |
| 152 const alpha = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefg
hijklmnopqrstuvwxyz0123456789!@#$%^&*()" |
| 153 data[i] = alpha[i%len(alpha)] |
| 154 } |
| 155 o.BigData = data |
| 156 |
| 157 So(ds.Put(&o), ShouldBeNil) |
| 158 So(ds.Get(&o), ShouldBeNil) |
| 159 |
| 160 itm := itmFor(0, ds.KeyForObj(&o)) |
| 161 So(mc.Get(itm), ShouldBeNil) |
| 162 |
| 163 So(itm.Value()[0], ShouldEqual, ZlibCompression) |
| 164 So(len(itm.Value()), ShouldEqual, 653) // a bit smaller
than 4k |
| 165 |
| 166 // ensure the next Get comes from the cache |
| 167 So(dsUnder.Delete(ds.KeyForObj(&o)), ShouldBeNil) |
| 168 |
| 169 o = object{ID: 2} |
| 170 So(ds.Get(&o), ShouldBeNil) |
| 171 So(o.Value, ShouldEqual, `¯\_(ツ)_/¯`) |
| 172 So(o.BigData, ShouldResemble, data) |
| 173 }) |
| 174 |
| 175 Convey("transactions", func() { |
| 176 Convey("work", func() { |
| 177 // populate an object @ ID1 |
| 178 So(ds.Put(&object{ID: 1, Value: "something"}), S
houldBeNil) |
| 179 So(ds.Get(&object{ID: 1}), ShouldBeNil) |
| 180 |
| 181 So(ds.Put(&object{ID: 2, Value: "nurbs"}), Shoul
dBeNil) |
| 182 So(ds.Get(&object{ID: 2}), ShouldBeNil) |
| 183 |
| 184 // memcache now has the wrong value (simulated r
ace) |
| 185 So(dsUnder.Put(&object{ID: 1, Value: "else"}), S
houldBeNil) |
| 186 So(ds.RunInTransaction(func(c context.Context) e
rror { |
| 187 ds := datastore.Get(c) |
| 188 o := &object{ID: 1} |
| 189 So(ds.Get(o), ShouldBeNil) |
| 190 So(o.Value, ShouldEqual, "else") |
| 191 o.Value = "txn" |
| 192 So(ds.Put(o), ShouldBeNil) |
| 193 |
| 194 So(ds.Delete(ds.KeyForObj(&object{ID: 2}
)), ShouldBeNil) |
| 195 return nil |
| 196 }, &datastore.TransactionOptions{XG: true}), Sho
uldBeNil) |
| 197 |
| 198 So(mc.Get(itmFor(0, ds.KeyForObj(&object{ID: 1})
)), |
| 199 ShouldEqual, memcache.ErrCacheMiss) |
| 200 So(mc.Get(itmFor(0, ds.KeyForObj(&object{ID: 2})
)), |
| 201 ShouldEqual, memcache.ErrCacheMiss) |
| 202 o := &object{ID: 1} |
| 203 So(ds.Get(o), ShouldBeNil) |
| 204 So(o.Value, ShouldEqual, "txn") |
| 205 }) |
| 206 |
| 207 Convey("errors don't invalidate", func() { |
| 208 // populate an object @ ID1 |
| 209 So(ds.Put(&object{ID: 1, Value: "something"}), S
houldBeNil) |
| 210 So(ds.Get(&object{ID: 1}), ShouldBeNil) |
| 211 So(numMemcacheItems(), ShouldEqual, 1) |
| 212 |
| 213 So(ds.RunInTransaction(func(c context.Context) e
rror { |
| 214 ds := datastore.Get(c) |
| 215 o := &object{ID: 1} |
| 216 So(ds.Get(o), ShouldBeNil) |
| 217 So(o.Value, ShouldEqual, "something") |
| 218 o.Value = "txn" |
| 219 So(ds.Put(o), ShouldBeNil) |
| 220 return errors.New("OH NOES") |
| 221 }, nil).Error(), ShouldContainSubstring, "OH NOE
S") |
| 222 |
| 223 // memcache still has the original |
| 224 So(numMemcacheItems(), ShouldEqual, 1) |
| 225 So(dsUnder.Delete(ds.KeyForObj(&object{ID: 1})),
ShouldBeNil) |
| 226 o := &object{ID: 1} |
| 227 So(ds.Get(o), ShouldBeNil) |
| 228 So(o.Value, ShouldEqual, "something") |
| 229 }) |
| 230 }) |
| 231 |
| 232 Convey("control", func() { |
| 233 Convey("per-model bypass", func() { |
| 234 type model struct { |
| 235 ID string `gae:"$id"` |
| 236 UseDSCache datastore.Toggle `gae:"$dscac
he.enable,false"` |
| 237 |
| 238 Value string |
| 239 } |
| 240 |
| 241 itms := []model{ |
| 242 {ID: "hi", Value: "something"}, |
| 243 {ID: "there", Value: "else", UseDSCache:
datastore.On}, |
| 244 } |
| 245 |
| 246 So(ds.PutMulti(itms), ShouldBeNil) |
| 247 So(ds.GetMulti(itms), ShouldBeNil) |
| 248 |
| 249 So(numMemcacheItems(), ShouldEqual, 1) |
| 250 }) |
| 251 |
| 252 Convey("per-key shard count", func() { |
| 253 s := &shardObj{ID: 4, Value: "hi"} |
| 254 So(ds.Put(s), ShouldBeNil) |
| 255 So(ds.Get(s), ShouldBeNil) |
| 256 |
| 257 So(numMemcacheItems(), ShouldEqual, 1) |
| 258 for i := 0; i < 20; i++ { |
| 259 So(ds.Get(s), ShouldBeNil) |
| 260 } |
| 261 So(numMemcacheItems(), ShouldEqual, 4) |
| 262 }) |
| 263 |
| 264 Convey("per-key cache disablement", func() { |
| 265 n := &noCacheObj{ID: "nurbs", Value: true} |
| 266 So(ds.Put(n), ShouldBeNil) |
| 267 So(ds.Get(n), ShouldBeNil) |
| 268 So(numMemcacheItems(), ShouldEqual, 0) |
| 269 }) |
| 270 |
| 271 Convey("per-model expiration", func() { |
| 272 type model struct { |
| 273 ID int64 `gae:"$id"` |
| 274 DSCacheExp int64 `gae:"$dscache.expirati
on,7"` |
| 275 |
| 276 Value string |
| 277 } |
| 278 |
| 279 So(ds.Put(&model{ID: 1, Value: "mooo"}), ShouldB
eNil) |
| 280 So(ds.Get(&model{ID: 1}), ShouldBeNil) |
| 281 |
| 282 itm := itmFor(0, ds.KeyForObj(&model{ID: 1})) |
| 283 So(mc.Get(itm), ShouldBeNil) |
| 284 |
| 285 clk.Add(10 * time.Second) |
| 286 So(mc.Get(itm), ShouldEqual, memcache.ErrCacheMi
ss) |
| 287 }) |
| 288 }) |
| 289 |
| 290 Convey("screw cases", func() { |
| 291 Convey("memcache contains bogus value (simulated failed
AddMulti)", func() { |
| 292 o := &object{ID: 1, Value: "spleen"} |
| 293 So(ds.Put(o), ShouldBeNil) |
| 294 |
| 295 sekret := []byte("I am a banana") |
| 296 itm := itmFor(0, ds.KeyForObj(o)).SetValue(sekre
t) |
| 297 So(mc.Set(itm), ShouldBeNil) |
| 298 |
| 299 o = &object{ID: 1} |
| 300 So(ds.Get(o), ShouldBeNil) |
| 301 So(o.Value, ShouldEqual, "spleen") |
| 302 |
| 303 So(mc.Get(itm), ShouldBeNil) |
| 304 So(itm.Flags(), ShouldEqual, ItemUKNONWN) |
| 305 So(itm.Value(), ShouldResemble, sekret) |
| 306 }) |
| 307 |
| 308 Convey("memcache contains bogus value (corrupt entry)",
func() { |
| 309 o := &object{ID: 1, Value: "spleen"} |
| 310 So(ds.Put(o), ShouldBeNil) |
| 311 |
| 312 sekret := []byte("I am a banana") |
| 313 itm := (itmFor(0, ds.KeyForObj(o)). |
| 314 SetValue(sekret). |
| 315 SetFlags(uint32(ItemHasData))) |
| 316 So(mc.Set(itm), ShouldBeNil) |
| 317 |
| 318 o = &object{ID: 1} |
| 319 So(ds.Get(o), ShouldBeNil) |
| 320 So(o.Value, ShouldEqual, "spleen") |
| 321 |
| 322 So(mc.Get(itm), ShouldBeNil) |
| 323 So(itm.Flags(), ShouldEqual, ItemHasData) |
| 324 So(itm.Value(), ShouldResemble, sekret) |
| 325 }) |
| 326 |
| 327 Convey("other entity has the lock", func() { |
| 328 o := &object{ID: 1, Value: "spleen"} |
| 329 So(ds.Put(o), ShouldBeNil) |
| 330 |
| 331 sekret := []byte("r@vmarod!#)%9T") |
| 332 itm := (itmFor(0, ds.KeyForObj(o)). |
| 333 SetValue(sekret). |
| 334 SetFlags(uint32(ItemHasLock))) |
| 335 So(mc.Set(itm), ShouldBeNil) |
| 336 |
| 337 o = &object{ID: 1} |
| 338 So(ds.Get(o), ShouldBeNil) |
| 339 So(o.Value, ShouldEqual, "spleen") |
| 340 |
| 341 So(mc.Get(itm), ShouldBeNil) |
| 342 So(itm.Flags(), ShouldEqual, ItemHasLock) |
| 343 So(itm.Value(), ShouldResemble, sekret) |
| 344 }) |
| 345 |
| 346 Convey("massive entities can't be cached", func() { |
| 347 o := &object{ID: 1, Value: "spleen"} |
| 348 mr := mathrand.Get(c) |
| 349 numRounds := (internalValueSizeLimit / 8) * 2 |
| 350 buf := bytes.Buffer{} |
| 351 for i := 0; i < numRounds; i++ { |
| 352 So(binary.Write(&buf, binary.LittleEndia
n, mr.Int63()), ShouldBeNil) |
| 353 } |
| 354 o.BigData = buf.Bytes() |
| 355 Println(buf.Len()) |
| 356 So(ds.Put(o), ShouldBeNil) |
| 357 |
| 358 o.BigData = nil |
| 359 So(ds.Get(o), ShouldBeNil) |
| 360 |
| 361 itm := itmFor(0, ds.KeyForObj(o)) |
| 362 So(mc.Get(itm), ShouldBeNil) |
| 363 |
| 364 // Is locked until the next put, forcing all acc
ess to the datastore. |
| 365 So(itm.Value(), ShouldResemble, []byte("M\x90\xc
1\xe8z$\xe3<")) |
| 366 So(itm.Flags(), ShouldEqual, ItemHasLock) |
| 367 |
| 368 o.BigData = []byte("hi :)") |
| 369 So(ds.Put(o), ShouldBeNil) |
| 370 So(ds.Get(o), ShouldBeNil) |
| 371 |
| 372 So(mc.Get(itm), ShouldBeNil) |
| 373 So(itm.Flags(), ShouldEqual, ItemHasData) |
| 374 }) |
| 375 |
| 376 }) |
| 377 |
| 378 Convey("misc", func() { |
| 379 Convey("verify numShards caps at MaxShards", func() { |
| 380 sc := supportContext{shardsForKey: shardsForKey} |
| 381 So(sc.numShards(ds.KeyForObj(&shardObj{ID: 9001}
)), ShouldEqual, MaxShards) |
| 382 }) |
| 383 |
| 384 Convey("CompressionType.String", func() { |
| 385 So(NoCompression.String(), ShouldEqual, "NoCompr
ession") |
| 386 So(ZlibCompression.String(), ShouldEqual, "ZlibC
ompression") |
| 387 So(CompressionType(100).String(), ShouldEqual, "
UNKNOWN_CompressionType(100)") |
| 388 }) |
| 389 }) |
| 390 }) |
| 391 } |
OLD | NEW |