github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/utils/parallel/try_test.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package parallel_test
     5  
     6  import (
     7  	"errors"
     8  	"fmt"
     9  	"io"
    10  	"sort"
    11  	"sync"
    12  	"time"
    13  
    14  	gc "launchpad.net/gocheck"
    15  
    16  	"launchpad.net/juju-core/testing"
    17  	jc "launchpad.net/juju-core/testing/checkers"
    18  	"launchpad.net/juju-core/utils/parallel"
    19  )
    20  
    21  type result string
    22  
    23  func (r result) Close() error {
    24  	return nil
    25  }
    26  
    27  type trySuite struct{}
    28  
    29  var _ = gc.Suite(&trySuite{})
    30  
    31  func tryFunc(delay time.Duration, val io.Closer, err error) func(<-chan struct{}) (io.Closer, error) {
    32  	return func(<-chan struct{}) (io.Closer, error) {
    33  		time.Sleep(delay)
    34  		return val, err
    35  	}
    36  }
    37  
    38  func (*trySuite) TestOneSuccess(c *gc.C) {
    39  	try := parallel.NewTry(0, nil)
    40  	try.Start(tryFunc(0, result("hello"), nil))
    41  	val, err := try.Result()
    42  	c.Assert(err, gc.IsNil)
    43  	c.Assert(val, gc.Equals, result("hello"))
    44  }
    45  
    46  func (*trySuite) TestOneFailure(c *gc.C) {
    47  	try := parallel.NewTry(0, nil)
    48  	expectErr := errors.New("foo")
    49  	err := try.Start(tryFunc(0, nil, expectErr))
    50  	c.Assert(err, gc.IsNil)
    51  	select {
    52  	case <-try.Dead():
    53  		c.Fatalf("try died before it should")
    54  	case <-time.After(testing.ShortWait):
    55  	}
    56  	try.Close()
    57  	select {
    58  	case <-try.Dead():
    59  	case <-time.After(testing.LongWait):
    60  		c.Fatalf("timed out waiting for Try to complete")
    61  	}
    62  	val, err := try.Result()
    63  	c.Assert(val, gc.IsNil)
    64  	c.Assert(err, gc.Equals, expectErr)
    65  }
    66  
    67  func (*trySuite) TestStartReturnsErrorAfterClose(c *gc.C) {
    68  	try := parallel.NewTry(0, nil)
    69  	expectErr := errors.New("foo")
    70  	err := try.Start(tryFunc(0, nil, expectErr))
    71  	c.Assert(err, gc.IsNil)
    72  	try.Close()
    73  	err = try.Start(tryFunc(0, result("goodbye"), nil))
    74  	c.Assert(err, gc.Equals, parallel.ErrClosed)
    75  	// Wait for the first try to deliver its result
    76  	time.Sleep(testing.ShortWait)
    77  	try.Kill()
    78  	err = try.Wait()
    79  	c.Assert(err, gc.Equals, expectErr)
    80  }
    81  
    82  func (*trySuite) TestOutOfOrderResults(c *gc.C) {
    83  	try := parallel.NewTry(0, nil)
    84  	try.Start(tryFunc(50*time.Millisecond, result("first"), nil))
    85  	try.Start(tryFunc(10*time.Millisecond, result("second"), nil))
    86  	r, err := try.Result()
    87  	c.Assert(err, gc.IsNil)
    88  	c.Assert(r, gc.Equals, result("second"))
    89  }
    90  
    91  func (*trySuite) TestMaxParallel(c *gc.C) {
    92  	try := parallel.NewTry(3, nil)
    93  	var (
    94  		mu    sync.Mutex
    95  		count int
    96  		max   int
    97  	)
    98  
    99  	for i := 0; i < 10; i++ {
   100  		try.Start(func(<-chan struct{}) (io.Closer, error) {
   101  			mu.Lock()
   102  			if count++; count > max {
   103  				max = count
   104  			}
   105  			c.Check(count, gc.Not(jc.GreaterThan), 3)
   106  			mu.Unlock()
   107  			time.Sleep(20 * time.Millisecond)
   108  			mu.Lock()
   109  			count--
   110  			mu.Unlock()
   111  			return result("hello"), nil
   112  		})
   113  	}
   114  	r, err := try.Result()
   115  	c.Assert(err, gc.IsNil)
   116  	c.Assert(r, gc.Equals, result("hello"))
   117  	mu.Lock()
   118  	defer mu.Unlock()
   119  	c.Assert(max, gc.Equals, 3)
   120  }
   121  
   122  func (*trySuite) TestStartBlocksForMaxParallel(c *gc.C) {
   123  	try := parallel.NewTry(3, nil)
   124  
   125  	started := make(chan struct{})
   126  	begin := make(chan struct{})
   127  	go func() {
   128  		for i := 0; i < 6; i++ {
   129  			err := try.Start(func(<-chan struct{}) (io.Closer, error) {
   130  				<-begin
   131  				return nil, fmt.Errorf("an error")
   132  			})
   133  			started <- struct{}{}
   134  			if i < 5 {
   135  				c.Check(err, gc.IsNil)
   136  			} else {
   137  				c.Check(err, gc.Equals, parallel.ErrClosed)
   138  			}
   139  		}
   140  		close(started)
   141  	}()
   142  	// Check we can start the first three.
   143  	timeout := time.After(testing.LongWait)
   144  	for i := 0; i < 3; i++ {
   145  		select {
   146  		case <-started:
   147  		case <-timeout:
   148  			c.Fatalf("timed out")
   149  		}
   150  	}
   151  	// Check we block when going above maxParallel.
   152  	timeout = time.After(testing.ShortWait)
   153  	select {
   154  	case <-started:
   155  		c.Fatalf("Start did not block")
   156  	case <-timeout:
   157  	}
   158  
   159  	// Unblock two attempts.
   160  	begin <- struct{}{}
   161  	begin <- struct{}{}
   162  
   163  	// Check we can start another two.
   164  	timeout = time.After(testing.LongWait)
   165  	for i := 0; i < 2; i++ {
   166  		select {
   167  		case <-started:
   168  		case <-timeout:
   169  			c.Fatalf("timed out")
   170  		}
   171  	}
   172  
   173  	// Check we block again when going above maxParallel.
   174  	timeout = time.After(testing.ShortWait)
   175  	select {
   176  	case <-started:
   177  		c.Fatalf("Start did not block")
   178  	case <-timeout:
   179  	}
   180  
   181  	// Close the Try - the last request should be discarded,
   182  	// unblocking last remaining Start request.
   183  	try.Close()
   184  
   185  	timeout = time.After(testing.LongWait)
   186  	select {
   187  	case <-started:
   188  	case <-timeout:
   189  		c.Fatalf("Start did not unblock after Close")
   190  	}
   191  
   192  	// Ensure all checks are completed
   193  	select {
   194  	case _, ok := <-started:
   195  		c.Assert(ok, gc.Equals, false)
   196  	case <-timeout:
   197  		c.Fatalf("Start goroutine did not finish")
   198  	}
   199  }
   200  
   201  func (*trySuite) TestAllConcurrent(c *gc.C) {
   202  	try := parallel.NewTry(0, nil)
   203  	started := make(chan chan struct{})
   204  	for i := 0; i < 10; i++ {
   205  		try.Start(func(<-chan struct{}) (io.Closer, error) {
   206  			reply := make(chan struct{})
   207  			started <- reply
   208  			<-reply
   209  			return result("hello"), nil
   210  		})
   211  	}
   212  	timeout := time.After(testing.LongWait)
   213  	for i := 0; i < 10; i++ {
   214  		select {
   215  		case reply := <-started:
   216  			reply <- struct{}{}
   217  		case <-timeout:
   218  			c.Fatalf("timed out")
   219  		}
   220  	}
   221  }
   222  
   223  type gradedError int
   224  
   225  func (e gradedError) Error() string {
   226  	return fmt.Sprintf("error with importance %d", e)
   227  }
   228  
   229  func gradedErrorCombine(err0, err1 error) error {
   230  	if err0 == nil || err0.(gradedError) < err1.(gradedError) {
   231  		return err1
   232  	}
   233  	return err0
   234  }
   235  
   236  type multiError struct {
   237  	errs []int
   238  }
   239  
   240  func (e *multiError) Error() string {
   241  	return fmt.Sprintf("%v", e.errs)
   242  }
   243  
   244  func (*trySuite) TestErrorCombine(c *gc.C) {
   245  	// Use maxParallel=1 to guarantee that all errors are processed sequentially.
   246  	try := parallel.NewTry(1, func(err0, err1 error) error {
   247  		if err0 == nil {
   248  			err0 = &multiError{}
   249  		}
   250  		err0.(*multiError).errs = append(err0.(*multiError).errs, int(err1.(gradedError)))
   251  		return err0
   252  	})
   253  	errors := []gradedError{3, 2, 4, 0, 5, 5, 3}
   254  	for _, err := range errors {
   255  		err := err
   256  		try.Start(func(<-chan struct{}) (io.Closer, error) {
   257  			return nil, err
   258  		})
   259  	}
   260  	try.Close()
   261  	val, err := try.Result()
   262  	c.Assert(val, gc.IsNil)
   263  	grades := err.(*multiError).errs
   264  	sort.Ints(grades)
   265  	c.Assert(grades, gc.DeepEquals, []int{0, 2, 3, 3, 4, 5, 5})
   266  }
   267  
   268  func (*trySuite) TestTriesAreStopped(c *gc.C) {
   269  	try := parallel.NewTry(0, nil)
   270  	stopped := make(chan struct{})
   271  	try.Start(func(stop <-chan struct{}) (io.Closer, error) {
   272  		<-stop
   273  		stopped <- struct{}{}
   274  		return nil, parallel.ErrStopped
   275  	})
   276  	try.Start(tryFunc(0, result("hello"), nil))
   277  	val, err := try.Result()
   278  	c.Assert(err, gc.IsNil)
   279  	c.Assert(val, gc.Equals, result("hello"))
   280  
   281  	select {
   282  	case <-stopped:
   283  	case <-time.After(testing.LongWait):
   284  		c.Fatalf("timed out waiting for stop")
   285  	}
   286  }
   287  
   288  func (*trySuite) TestCloseTwice(c *gc.C) {
   289  	try := parallel.NewTry(0, nil)
   290  	try.Close()
   291  	try.Close()
   292  	val, err := try.Result()
   293  	c.Assert(val, gc.IsNil)
   294  	c.Assert(err, gc.IsNil)
   295  }
   296  
   297  type closeResult struct {
   298  	closed chan struct{}
   299  }
   300  
   301  func (r *closeResult) Close() error {
   302  	close(r.closed)
   303  	return nil
   304  }
   305  
   306  func (*trySuite) TestExtraResultsAreClosed(c *gc.C) {
   307  	try := parallel.NewTry(0, nil)
   308  	begin := make([]chan struct{}, 4)
   309  	results := make([]*closeResult, len(begin))
   310  	for i := range begin {
   311  		begin[i] = make(chan struct{})
   312  		results[i] = &closeResult{make(chan struct{})}
   313  		i := i
   314  		try.Start(func(<-chan struct{}) (io.Closer, error) {
   315  			<-begin[i]
   316  			return results[i], nil
   317  		})
   318  	}
   319  	begin[0] <- struct{}{}
   320  	val, err := try.Result()
   321  	c.Assert(err, gc.IsNil)
   322  	c.Assert(val, gc.Equals, results[0])
   323  
   324  	timeout := time.After(testing.ShortWait)
   325  	for i, r := range results[1:] {
   326  		begin[i+1] <- struct{}{}
   327  		select {
   328  		case <-r.closed:
   329  		case <-timeout:
   330  			c.Fatalf("timed out waiting for close")
   331  		}
   332  	}
   333  	select {
   334  	case <-results[0].closed:
   335  		c.Fatalf("result was inappropriately closed")
   336  	case <-time.After(testing.ShortWait):
   337  	}
   338  }
   339  
   340  func (*trySuite) TestEverything(c *gc.C) {
   341  	try := parallel.NewTry(5, gradedErrorCombine)
   342  	tries := []struct {
   343  		startAt time.Duration
   344  		wait    time.Duration
   345  		val     result
   346  		err     error
   347  	}{{
   348  		wait: 30 * time.Millisecond,
   349  		err:  gradedError(3),
   350  	}, {
   351  		startAt: 10 * time.Millisecond,
   352  		wait:    20 * time.Millisecond,
   353  		val:     result("result 1"),
   354  	}, {
   355  		startAt: 20 * time.Millisecond,
   356  		wait:    10 * time.Millisecond,
   357  		val:     result("result 2"),
   358  	}, {
   359  		startAt: 20 * time.Millisecond,
   360  		wait:    5 * time.Second,
   361  		val:     "delayed result",
   362  	}, {
   363  		startAt: 5 * time.Millisecond,
   364  		err:     gradedError(4),
   365  	}}
   366  	for _, t := range tries {
   367  		t := t
   368  		go func() {
   369  			time.Sleep(t.startAt)
   370  			try.Start(tryFunc(t.wait, t.val, t.err))
   371  		}()
   372  	}
   373  	val, err := try.Result()
   374  	if val != result("result 1") && val != result("result 2") {
   375  		c.Errorf(`expected "result 1" or "result 2" got %#v`, val)
   376  	}
   377  	c.Assert(err, gc.IsNil)
   378  }