go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/service/datastore/batcher_test.go (about) 1 // Copyright 2016 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 datastore 16 17 import ( 18 "context" 19 "fmt" 20 "sync" 21 "sync/atomic" 22 "testing" 23 24 "go.chromium.org/luci/gae/service/info" 25 26 . "github.com/smartystreets/goconvey/convey" 27 ) 28 29 type counterFilter struct { 30 run int32 31 put int32 32 get int32 33 delete int32 34 } 35 36 func (cf *counterFilter) filter() RawFilter { 37 return func(c context.Context, rds RawInterface) RawInterface { 38 return &counterFilterInst{ 39 RawInterface: rds, 40 counterFilter: cf, 41 } 42 } 43 } 44 45 type counterFilterInst struct { 46 RawInterface 47 *counterFilter 48 } 49 50 func (rc *counterFilterInst) Run(fq *FinalizedQuery, cb RawRunCB) error { 51 atomic.AddInt32(&rc.run, 1) 52 return rc.RawInterface.Run(fq, cb) 53 } 54 55 func (rc *counterFilterInst) PutMulti(keys []*Key, pmap []PropertyMap, cb NewKeyCB) error { 56 atomic.AddInt32(&rc.put, 1) 57 return rc.RawInterface.PutMulti(keys, pmap, cb) 58 } 59 60 func (rc *counterFilterInst) GetMulti(keys []*Key, meta MultiMetaGetter, cb GetMultiCB) error { 61 atomic.AddInt32(&rc.get, 1) 62 return rc.RawInterface.GetMulti(keys, meta, cb) 63 } 64 65 func (rc *counterFilterInst) DeleteMulti(keys []*Key, cb DeleteMultiCB) error { 66 atomic.AddInt32(&rc.delete, 1) 67 return rc.RawInterface.DeleteMulti(keys, cb) 68 } 69 70 func TestQueryBatch(t *testing.T) { 71 t.Parallel() 72 73 Convey("A testing datastore with a data set installed", t, func() { 74 c := info.Set(context.Background(), fakeInfo{}) 75 76 fds := fakeDatastore{ 77 entities: 2048, 78 } 79 c = SetRawFactory(c, fds.factory()) 80 81 cf := counterFilter{} 82 c = AddRawFilters(c, cf.filter()) 83 84 // Given query batch size, how many Run calls will be executed to pull 85 // "total" results? 86 expectedBatchRunCalls := func(batchSize, total int32) int32 { 87 if batchSize <= 0 { 88 return 1 89 } 90 exp := total / batchSize 91 if total%batchSize != 0 { 92 exp++ 93 } 94 return exp 95 } 96 97 // Get all items in the query, then reset the counter. 98 all := []*CommonStruct(nil) 99 if err := GetAll(c, NewQuery(""), &all); err != nil { 100 panic(err) 101 } 102 cf.run = 0 103 104 for _, sizeBase := range []int32{ 105 1, 106 16, 107 1024, 108 2048, 109 } { 110 // Adjust to hit edge cases. 111 for _, delta := range []int32{-1, 0, 1} { 112 batchSize := sizeBase + delta 113 if batchSize <= 0 { 114 continue 115 } 116 117 getAllBatch := func(c context.Context, batchSize int32, query *Query) ([]*CommonStruct, error) { 118 var out []*CommonStruct 119 err := RunBatch(c, batchSize, query, func(cs *CommonStruct) { 120 out = append(out, cs) 121 }) 122 return out, err 123 } 124 125 Convey(fmt.Sprintf(`Batching with size %d installed`, batchSize), func() { 126 q := NewQuery("") 127 128 Convey(`Can retrieve all of the items.`, func() { 129 got, err := getAllBatch(c, batchSize, q) 130 So(err, ShouldBeNil) 131 So(got, ShouldResemble, all) 132 133 // One call for every sub-query, plus one to hit Stop. 134 runCalls := (int32(len(all)) / batchSize) + 1 135 So(cf.run, ShouldEqual, runCalls) 136 }) 137 138 Convey(`With a limit of 128, will retrieve 128 items.`, func() { 139 const limit = 128 140 q = q.Limit(int32(limit)) 141 142 got, err := getAllBatch(c, batchSize, q) 143 So(err, ShouldBeNil) 144 So(got, ShouldResemble, all[:limit]) 145 146 So(cf.run, ShouldEqual, expectedBatchRunCalls(batchSize, limit)) 147 }) 148 }) 149 } 150 } 151 152 Convey(`Test iterative Run with cursors.`, func() { 153 // This test will have a naive outer loop that fetches pages in large 154 // increments using cursors. The outer loop will use the Batcher 155 // internally, which will fetch smaller page sizes. 156 testIterativeRun := func(rounds, outerFetchSize, batchSize int32) error { 157 // Clear state and configure. 158 cf.run = 0 159 fds.entities = rounds * outerFetchSize 160 161 var ( 162 outerCount int32 163 cursor Cursor 164 ) 165 for i := int32(0); i < rounds; i++ { 166 // Fetch "outerFetchSize" items from our Batcher. 167 q := NewQuery("").Limit(outerFetchSize) 168 if cursor != nil { 169 q = q.Start(cursor) 170 } 171 172 err := RunBatch(c, batchSize, q, func(v CommonStruct, getCursor CursorCB) (err error) { 173 if v.Value != int64(outerCount) { 174 return fmt.Errorf("query value doesn't match count (%d != %d)", v.Value, outerCount) 175 } 176 outerCount++ 177 178 // Retain our cursor from this round. 179 cursor, err = getCursor() 180 return 181 }) 182 if err != nil { 183 return err 184 } 185 } 186 187 // Make sure we iterated through everything. 188 if outerCount != fds.entities { 189 return fmt.Errorf("query returned incomplete results (%d != %d)", outerCount, fds.entities) 190 } 191 192 // Make sure the appropriate number of real queries was executed. 193 expectedRunCount := expectedBatchRunCalls(batchSize, outerFetchSize) * rounds 194 if cf.run != expectedRunCount { 195 return fmt.Errorf("unexpected number of raw Run calls (%d != %d)", cf.run, expectedRunCount) 196 } 197 return nil 198 } 199 200 So(testIterativeRun(3, 2, 1), ShouldBeNil) 201 So(testIterativeRun(3, 5, 2), ShouldBeNil) 202 So(testIterativeRun(3, 1000, 250), ShouldBeNil) 203 204 // We'll use fetch/batch sizes that are not direct multiples of each other 205 // so we can test some incongruent boundaries. 206 So(testIterativeRun(3, 900, 250), ShouldBeNil) 207 }) 208 }) 209 } 210 211 func TestBatchFilter(t *testing.T) { 212 t.Parallel() 213 214 type IndexEntity struct { 215 _kind string `gae:"$kind,Index"` 216 217 Key *Key `gae:"$key"` 218 Value int64 219 } 220 221 Convey("A testing datastore", t, func() { 222 c := info.Set(context.Background(), fakeInfo{}) 223 224 fds := fakeDatastore{} 225 c = SetRawFactory(c, fds.factory()) 226 227 cf := counterFilter{} 228 c = AddRawFilters(c, cf.filter()) 229 230 expectedRounds := func(constraint, size int) int { 231 v := size / constraint 232 if size%constraint != 0 { 233 v++ 234 } 235 return v 236 } 237 238 for _, sz := range []int32{11, 10, 7, 5, 2} { 239 Convey(fmt.Sprintf("With maximunm Put size %d", sz), func(convey C) { 240 fds.convey = convey 241 fds.constraints.MaxGetSize = 10 242 fds.constraints.MaxPutSize = 10 243 fds.constraints.MaxDeleteSize = 10 244 245 css := make([]*IndexEntity, 10) 246 for i := range css { 247 css[i] = &IndexEntity{Value: int64(i + 1)} 248 } 249 250 So(Put(c, css), ShouldBeNil) 251 So(cf.put, ShouldEqual, expectedRounds(fds.constraints.MaxPutSize, len(css))) 252 253 for i, ent := range css { 254 So(ent.Key, ShouldNotBeNil) 255 So(ent.Key.IntID(), ShouldEqual, i+1) 256 } 257 258 Convey(`Get`, func() { 259 // Clear Value and Get, populating Value from Key.IntID. 260 for _, ent := range css { 261 ent.Value = 0 262 } 263 264 So(Get(c, css), ShouldBeNil) 265 So(cf.get, ShouldEqual, expectedRounds(fds.constraints.MaxGetSize, len(css))) 266 267 for i, ent := range css { 268 So(ent.Value, ShouldEqual, i+1) 269 } 270 }) 271 272 Convey(`Delete`, func() { 273 // Record which entities get deleted. 274 var lock sync.Mutex 275 deleted := make(map[int64]struct{}, len(css)) 276 fds.onDelete = func(k *Key) { 277 lock.Lock() 278 defer lock.Unlock() 279 deleted[k.IntID()] = struct{}{} 280 } 281 282 So(Delete(c, css), ShouldBeNil) 283 So(cf.delete, ShouldEqual, expectedRounds(fds.constraints.MaxDeleteSize, len(css))) 284 285 // Confirm that all entities have been deleted. 286 So(len(deleted), ShouldEqual, len(css)) 287 for i := range css { 288 _, ok := deleted[int64(i+1)] 289 So(ok, ShouldBeTrue) 290 } 291 }) 292 }) 293 } 294 }) 295 }