Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(95)

Side by Side Diff: filter/dscache/dscache_test.go

Issue 1269113005: A transparent cache for datastore, backed by memcache. (Closed) Base URL: https://github.com/luci/gae.git@add_meta
Patch Set: some minor comments Created 5 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
(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 }
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698