go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cv/internal/common/eventbox/dsset/dsset_test.go (about)

     1  // Copyright 2017 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 dsset
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"math/rand"
    21  	"sync"
    22  	"testing"
    23  	"time"
    24  
    25  	"go.chromium.org/luci/gae/filter/txndefer"
    26  	"go.chromium.org/luci/gae/impl/memory"
    27  	"go.chromium.org/luci/gae/service/datastore"
    28  
    29  	"go.chromium.org/luci/common/clock"
    30  	"go.chromium.org/luci/common/clock/testclock"
    31  	"go.chromium.org/luci/common/data/rand/mathrand"
    32  	"go.chromium.org/luci/common/data/stringset"
    33  
    34  	. "github.com/smartystreets/goconvey/convey"
    35  )
    36  
    37  func testingContext() context.Context {
    38  	c := txndefer.FilterRDS(memory.Use(context.Background()))
    39  	datastore.GetTestable(c).AutoIndex(true)
    40  	datastore.GetTestable(c).Consistent(true)
    41  	c = clock.Set(c, testclock.New(time.Unix(1442270520, 0).UTC()))
    42  	c = mathrand.Set(c, rand.New(rand.NewSource(1000)))
    43  	return c
    44  }
    45  
    46  // pop pops a bunch of items from the set and returns items that were popped.
    47  func pop(c context.Context, s *Set, listing *Listing, ids []string) (popped []string, err error) {
    48  	op, err := s.BeginPop(c, listing)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  	for _, id := range ids {
    53  		if op.Pop(id) {
    54  			popped = append(popped, id)
    55  		}
    56  	}
    57  	if err = FinishPop(c, op); err != nil {
    58  		return nil, err
    59  	}
    60  	return popped, nil
    61  }
    62  
    63  func TestSet(t *testing.T) {
    64  	t.Parallel()
    65  
    66  	Convey("item one lifecycle", t, func() {
    67  		c := testingContext()
    68  
    69  		set := Set{
    70  			Parent:          datastore.NewKey(c, "Parent", "parent", 0, nil),
    71  			TombstonesDelay: time.Minute,
    72  		}
    73  		const limit = 10
    74  
    75  		// Add one item.
    76  		So(set.Add(c, []Item{{ID: "abc"}}), ShouldBeNil)
    77  
    78  		// The item is returned by the listing.
    79  		listing, err := set.List(c, limit)
    80  		So(err, ShouldBeNil)
    81  		So(listing.Items, ShouldResemble, []Item{{ID: "abc"}})
    82  		So(listing.Garbage, ShouldBeNil)
    83  
    84  		// Pop it!
    85  		err = datastore.RunInTransaction(c, func(c context.Context) error {
    86  			popped, err := pop(c, &set, listing, []string{"abc"})
    87  			So(err, ShouldBeNil)
    88  			So(popped, ShouldResemble, []string{"abc"})
    89  			return nil
    90  		}, nil)
    91  		So(err, ShouldBeNil)
    92  
    93  		// The listing no longer returns it.
    94  		listing, err = set.List(c, limit)
    95  		So(err, ShouldBeNil)
    96  		So(listing.Items, ShouldBeNil)
    97  
    98  		// The listing no longer returns the item, and there's no tombstones to
    99  		// cleanup.
   100  		listing, err = set.List(c, limit)
   101  		So(err, ShouldBeNil)
   102  		So(listing.Items, ShouldBeNil)
   103  		So(listing.Garbage, ShouldBeNil)
   104  
   105  		// Attempt to add it back (should be ignored).
   106  		So(set.Add(c, []Item{{ID: "abc"}}), ShouldBeNil)
   107  
   108  		// The listing still doesn't return it, but we now have a tombstone to
   109  		// cleanup (again).
   110  		listing, err = set.List(c, limit)
   111  		So(err, ShouldBeNil)
   112  		So(listing.Items, ShouldBeNil)
   113  		So(len(listing.Garbage), ShouldEqual, 1)
   114  		So(listing.Garbage[0].old, ShouldBeFalse)
   115  		So(listing.Garbage[0].storage, ShouldNotBeNil)
   116  
   117  		// Popping it again doesn't work either.
   118  		err = datastore.RunInTransaction(c, func(c context.Context) error {
   119  			popped, err := pop(c, &set, listing, []string{"abc"})
   120  			So(err, ShouldBeNil)
   121  			So(popped, ShouldBeNil)
   122  			return nil
   123  		}, nil)
   124  		So(err, ShouldBeNil)
   125  
   126  		// Cleaning up the storage, again. This should make List stop returning
   127  		// the tombstone (since it has no storage items associated with it and it's
   128  		// not ready to be evicted yet).
   129  		So(CleanupGarbage(c, listing.Garbage), ShouldBeNil)
   130  		listing, err = set.List(c, limit)
   131  		So(err, ShouldBeNil)
   132  		So(listing.Items, ShouldBeNil)
   133  		So(listing.Garbage, ShouldBeNil)
   134  
   135  		// Time passes, tombstone expires.
   136  		clock.Get(c).(testclock.TestClock).Add(2 * time.Minute)
   137  
   138  		// Listing now returns expired tombstone.
   139  		listing, err = set.List(c, limit)
   140  		So(err, ShouldBeNil)
   141  		So(listing.Items, ShouldBeNil)
   142  		So(len(listing.Garbage), ShouldEqual, 1)
   143  		So(listing.Garbage[0].storage, ShouldBeNil) // cleaned already
   144  
   145  		// Cleanup storage keys.
   146  		So(CleanupGarbage(c, listing.Garbage), ShouldBeNil)
   147  
   148  		// Cleanup the tombstones themselves.
   149  		err = datastore.RunInTransaction(c, func(c context.Context) error {
   150  			popped, err := pop(c, &set, listing, nil)
   151  			So(err, ShouldBeNil)
   152  			So(popped, ShouldBeNil)
   153  			return nil
   154  		}, nil)
   155  		So(err, ShouldBeNil)
   156  
   157  		// No tombstones returned any longer.
   158  		listing, err = set.List(c, limit)
   159  		So(err, ShouldBeNil)
   160  		So(listing.Items, ShouldBeNil)
   161  		So(listing.Garbage, ShouldBeNil)
   162  
   163  		// And the item can be added back now, since no trace of it is left.
   164  		So(set.Add(c, []Item{{ID: "abc"}}), ShouldBeNil)
   165  
   166  		// Yep, it is there.
   167  		listing, err = set.List(c, limit)
   168  		So(err, ShouldBeNil)
   169  		So(listing.Items, ShouldResemble, []Item{{ID: "abc"}})
   170  		So(listing.Garbage, ShouldBeNil)
   171  	})
   172  
   173  	Convey("List obeys limit", t, func() {
   174  		c := testingContext()
   175  		set := Set{
   176  			Parent:          datastore.MakeKey(c, "Parent", "parent"),
   177  			TombstonesDelay: time.Minute,
   178  		}
   179  		So(set.Add(c, []Item{{ID: "abc"}}), ShouldBeNil)
   180  		So(set.Add(c, []Item{{ID: "def"}}), ShouldBeNil)
   181  		So(set.Add(c, []Item{{ID: "ghi"}}), ShouldBeNil)
   182  
   183  		l, err := set.List(c, 2)
   184  		So(err, ShouldBeNil)
   185  		So(l.Items, ShouldHaveLength, 2)
   186  	})
   187  
   188  	Convey("delete items non-transactionally", t, func() {
   189  		c := testingContext()
   190  
   191  		set := Set{
   192  			Parent:          datastore.MakeKey(c, "Parent", "parent"),
   193  			TombstonesDelay: time.Minute,
   194  		}
   195  
   196  		// Add 3 items.
   197  		So(set.Add(c, []Item{{ID: "abc"}}), ShouldBeNil)
   198  		So(set.Add(c, []Item{{ID: "def"}}), ShouldBeNil)
   199  		So(set.Add(c, []Item{{ID: "ghi"}}), ShouldBeNil)
   200  
   201  		l, err := set.List(c, 10)
   202  		So(err, ShouldBeNil)
   203  		So(l.Items, ShouldHaveLength, 3)
   204  
   205  		// Delete 2 items before transacting.
   206  		i := 0
   207  		err = set.Delete(c, func() string {
   208  			switch i = i + 1; i {
   209  			case 1:
   210  				return "def"
   211  			case 2:
   212  				return "abc"
   213  			default:
   214  				return ""
   215  			}
   216  		})
   217  		So(err, ShouldBeNil)
   218  
   219  		l2, err := set.List(c, 10)
   220  		So(err, ShouldBeNil)
   221  		So(l2.Items, ShouldResemble, []Item{{ID: "ghi"}})
   222  	})
   223  }
   224  
   225  func TestStress(t *testing.T) {
   226  	t.Parallel()
   227  
   228  	Convey("stress", t, func() {
   229  		// Add 1000 items in parallel from N goroutines, and (also in parallel),
   230  		// run N instances of "List and pop all", collecting the result in single
   231  		// list. There should be no duplicates in the final list!
   232  		c := testingContext()
   233  
   234  		set := Set{
   235  			Parent:          datastore.MakeKey(c, "Parent", "parent"),
   236  			TombstonesDelay: time.Minute,
   237  		}
   238  
   239  		producers := 3
   240  		consumers := 5
   241  		items := 100
   242  
   243  		wakeups := make(chan string)
   244  
   245  		lock := sync.Mutex{}
   246  		var consumed []string
   247  
   248  		for i := 0; i < producers; i++ {
   249  			go func() {
   250  				for j := 0; j < items; j++ {
   251  					set.Add(c, []Item{{ID: fmt.Sprintf("%d", j)}})
   252  					// Wake up 3 consumers, so they "fight".
   253  					wakeups <- "wake"
   254  					wakeups <- "wake"
   255  					wakeups <- "wake"
   256  				}
   257  				for i := 0; i < consumers; i++ {
   258  					wakeups <- "done"
   259  				}
   260  			}()
   261  		}
   262  
   263  		consume := func() {
   264  			listing, err := set.List(c, 100000)
   265  			if err != nil || len(listing.Items) == 0 {
   266  				return
   267  			}
   268  
   269  			keys := make([]string, len(listing.Items))
   270  			for i, itm := range listing.Items {
   271  				keys[i] = itm.ID
   272  			}
   273  
   274  			// Try to pop all.
   275  			var popped []string
   276  			err = datastore.RunInTransaction(c, func(c context.Context) error {
   277  				var err error
   278  				popped, err = pop(c, &set, listing, keys)
   279  				return err
   280  			}, nil)
   281  
   282  			// Consider items consumed only if transaction has landed.
   283  			if err == nil && len(popped) != 0 {
   284  				lock.Lock()
   285  				consumed = append(consumed, popped...)
   286  				lock.Unlock()
   287  			}
   288  		}
   289  
   290  		wg := sync.WaitGroup{}
   291  		wg.Add(consumers)
   292  		for i := 0; i < consumers; i++ {
   293  			go func() {
   294  				defer wg.Done()
   295  				done := false
   296  				for !done {
   297  					done = (<-wakeups) == "done"
   298  					consume()
   299  				}
   300  			}()
   301  		}
   302  
   303  		wg.Wait() // this waits for completion of the entire pipeline
   304  
   305  		// Make sure 'consumed' is the initially produced set.
   306  		dedup := stringset.New(len(consumed))
   307  		for _, itm := range consumed {
   308  			dedup.Add(itm)
   309  		}
   310  		So(dedup.Len(), ShouldEqual, len(consumed)) // no dups
   311  		So(len(consumed), ShouldEqual, items)       // all are accounted for
   312  	})
   313  }