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  }