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  }