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  }