go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/tq/internal/workset/workset_test.go (about) 1 // Copyright 2020 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 workset 16 17 import ( 18 "context" 19 "sync" 20 "testing" 21 22 . "github.com/smartystreets/goconvey/convey" 23 ) 24 25 func TestWorkSet(t *testing.T) { 26 t.Parallel() 27 28 Convey("Smoke test", t, func() { 29 const N = 100 30 31 items := []Item{} 32 for i := 0; i < N; i++ { 33 items = append(items, i) 34 } 35 ws := New(items, nil) 36 37 results := make(chan int, 1000) 38 39 // Worker takes a number K and 40 // If K < N: produces two numbers 2*K+N and 2*K+N+1. 41 // If N <= K < 2*N: produces a number K+2*N. 42 // As a result, when everything is done we should have processed 43 // a continuous range [0, 4*N). 44 worker := func(ctx context.Context) { 45 for { 46 item, done := ws.Pop(ctx) 47 if item == nil { 48 return 49 } 50 val := item.(int) 51 switch { 52 case val < N: 53 done([]Item{val*2 + N, val*2 + N + 1}) 54 case val < 2*N: 55 done([]Item{val + 2*N}) 56 default: 57 done(nil) // no new work 58 } 59 results <- val 60 } 61 } 62 63 checkResult := func() { 64 close(results) 65 processed := map[int]bool{} 66 for i := range results { 67 So(processed[i], ShouldBeFalse) // no dups 68 processed[i] = true 69 } 70 So(processed, ShouldHaveLength, 4*N) // finished everything 71 } 72 73 Convey("Single worker", func() { 74 worker(context.Background()) 75 checkResult() 76 }) 77 78 Convey("Many workers", func() { 79 wg := sync.WaitGroup{} 80 wg.Add(5) 81 for i := 0; i < 5; i++ { 82 go func() { 83 defer wg.Done() 84 worker(context.Background()) 85 }() 86 } 87 wg.Wait() 88 checkResult() 89 }) 90 }) 91 92 Convey("Many waiters", t, func() { 93 results := make(chan int, 1000) 94 95 ws := New([]Item{0}, nil) 96 97 startConsumers := make(chan struct{}) 98 99 // Consumer goroutines. 100 wg := sync.WaitGroup{} 101 wg.Add(5) 102 for i := 0; i < 5; i++ { 103 go func() { 104 defer wg.Done() 105 <-startConsumers 106 for { 107 item, done := ws.Pop(context.Background()) 108 if item == nil { 109 return 110 } 111 results <- item.(int) 112 done(nil) 113 } 114 }() 115 } 116 117 // The producer. 118 item, done := ws.Pop(context.Background()) 119 So(item.(int), ShouldEqual, 0) 120 close(startConsumers) 121 done([]Item{1, 2, 3, 4, 5, 6, 7, 8}) 122 123 wg.Wait() 124 125 close(results) 126 processed := map[int]bool{} 127 for i := range results { 128 So(processed[i], ShouldBeFalse) // no dups 129 processed[i] = true 130 } 131 So(len(processed), ShouldEqual, 8) 132 }) 133 134 Convey("Context cancellation", t, func() { 135 ws := New([]Item{0}, nil) 136 137 item, done := ws.Pop(context.Background()) 138 So(item.(int), ShouldEqual, 0) 139 140 got := make(chan Item, 1) 141 142 ctx, cancel := context.WithCancel(context.Background()) 143 go func() { 144 item, done := ws.Pop(ctx) 145 if done != nil { 146 panic("should be nil") 147 } 148 got <- item 149 }() 150 151 // The gorouting is stuck waiting until there are new items in the set (i.e. 152 // `done` is called) or the context is canceled. 153 cancel() 154 So(<-got, ShouldBeNil) // unblocked and finished with nil 155 156 // Make sure the workset is still functional. 157 go func() { 158 item, done := ws.Pop(context.Background()) 159 done(nil) 160 got <- item 161 }() 162 163 done([]Item{111}) 164 So((<-got).(int), ShouldEqual, 111) 165 }) 166 }