go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/common/sync/parallel/runner_test.go (about)

     1  // Copyright 2015 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 parallel
    16  
    17  import (
    18  	"errors"
    19  	"fmt"
    20  	"sync/atomic"
    21  	"testing"
    22  
    23  	. "github.com/smartystreets/goconvey/convey"
    24  	. "go.chromium.org/luci/common/testing/assertions"
    25  )
    26  
    27  type numberError int
    28  
    29  func (e numberError) Error() string {
    30  	return fmt.Sprintf("#%d", e)
    31  }
    32  
    33  func TestRunner(t *testing.T) {
    34  	t.Parallel()
    35  
    36  	Convey("When using a Runner directly", t, func() {
    37  		r := &Runner{}
    38  		defer func() {
    39  			if r != nil {
    40  				r.Close()
    41  			}
    42  		}()
    43  
    44  		Convey(`Can schedule individual tasks.`, func() {
    45  			const iters = 100
    46  
    47  			ac := int32(0)
    48  			resultC := make(chan int)
    49  
    50  			// Dispatch iters tasks.
    51  			for i := 0; i < iters; i++ {
    52  				i := i
    53  
    54  				errC := r.RunOne(func() error {
    55  					atomic.AddInt32(&ac, int32(i))
    56  					return numberError(i)
    57  				})
    58  
    59  				// Reap errC.
    60  				go func() {
    61  					resultC <- int((<-errC).(numberError))
    62  				}()
    63  			}
    64  
    65  			// Reap the results and compare.
    66  			result := 0
    67  			for i := 0; i < iters; i++ {
    68  				result += <-resultC
    69  			}
    70  			So(result, ShouldEqual, atomic.LoadInt32(&ac))
    71  		})
    72  
    73  		Convey(`Can use WorkC directly in a bidirectional select loop.`, func() {
    74  			// Generate a function that writes a value to "outC".
    75  			outC := make(chan int)
    76  			valueWriter := func(v int) func() error {
    77  				return func() error {
    78  					outC <- v
    79  					return nil
    80  				}
    81  			}
    82  
    83  			// We will repeatedly dispatch tasks and reap their results in the same
    84  			// select loop.
    85  			//
    86  			// remaining is the total number of tasks to dispatch. dispatch controls
    87  			// the number of simultaneous tasks that we're willing to send at a time.
    88  			// count is the number of tasks that have completed.
    89  			remaining := 1000
    90  			count := 0
    91  			dispatch := 10
    92  			wc := r.WorkC()
    93  
    94  			for count < 1000 {
    95  				select {
    96  				case wc <- WorkItem{F: valueWriter(1)}:
    97  					dispatch--
    98  					remaining--
    99  					break
   100  
   101  				case v := <-outC:
   102  					count += v
   103  					dispatch++
   104  				}
   105  
   106  				if dispatch == 0 || remaining == 0 {
   107  					// Stop writing work items until we reap some results.
   108  					wc = nil
   109  				} else {
   110  					// We have results, start writing work items again.
   111  					wc = r.WorkC()
   112  				}
   113  			}
   114  
   115  			So(count, ShouldEqual, 1000)
   116  		})
   117  
   118  		Convey(`A WorkItem's After method can recover from a panic.`, func() {
   119  			testErr := errors.New("test error")
   120  
   121  			var err error
   122  			r.WorkC() <- WorkItem{
   123  				F: func() error {
   124  					panic(testErr)
   125  				},
   126  				After: func() {
   127  					if r := recover(); r != nil {
   128  						err = r.(error)
   129  					}
   130  				},
   131  			}
   132  
   133  			r.Close()
   134  			r = nil // Do we don't close it in a defer.
   135  
   136  			So(err, ShouldEqual, testErr)
   137  		})
   138  
   139  		Convey("Ignore consumes the errors and blocks", func() {
   140  			count := new(int32)
   141  			Ignore(r.Run(func(ch chan<- func() error) {
   142  				for i := 0; i < 100; i++ {
   143  					ch <- func() error {
   144  						atomic.AddInt32(count, 1)
   145  						return fmt.Errorf("whaaattt")
   146  					}
   147  				}
   148  			}))
   149  			So(*count, ShouldEqual, 100)
   150  		})
   151  
   152  		Convey("Must panics on the first error", func() {
   153  			r.Maximum = 1
   154  			count := new(int32)
   155  
   156  			// Reap errC at the end, since Must will panic without consuming its
   157  			// contents.
   158  			var errC <-chan error
   159  			defer func() {
   160  				if errC != nil {
   161  					for range errC {
   162  					}
   163  				}
   164  			}()
   165  
   166  			So(func() {
   167  				errC = r.Run(func(ch chan<- func() error) {
   168  					for i := 0; i < 100; i++ {
   169  						i := i
   170  						ch <- func() error {
   171  							atomic.AddInt32(count, 1)
   172  							return fmt.Errorf("whaaattt: %d", i)
   173  						}
   174  					}
   175  				})
   176  				Must(errC)
   177  			}, ShouldPanicLike, "whaaattt: 0")
   178  			// Either:
   179  			//   * the panic happened and we load count before ch is unblocked
   180  			//   * the panic happened then ch(1) pushes and runs, then we load count
   181  			// So count will either be 1 or 2, but never more or less.
   182  			So(atomic.LoadInt32(count), ShouldBeBetweenOrEqual, 1, 2)
   183  		})
   184  	})
   185  }