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 }