github.com/fozzysec/SiaPrime@v0.0.0-20190612043147-66c8e8d11fe3/sync/threadgroup_test.go (about)

     1  package sync
     2  
     3  import (
     4  	"net"
     5  	"os"
     6  	"path/filepath"
     7  	"sync"
     8  	"testing"
     9  	"time"
    10  
    11  	"SiaPrime/build"
    12  )
    13  
    14  // TestThreadGroupStopEarly tests that a thread group can correctly interrupt
    15  // an ongoing process.
    16  func TestThreadGroupStopEarly(t *testing.T) {
    17  	if testing.Short() {
    18  		t.SkipNow()
    19  	}
    20  	t.Parallel()
    21  
    22  	var tg ThreadGroup
    23  	for i := 0; i < 10; i++ {
    24  		err := tg.Add()
    25  		if err != nil {
    26  			t.Fatal(err)
    27  		}
    28  
    29  		go func() {
    30  			defer tg.Done()
    31  			select {
    32  			case <-time.After(1 * time.Second):
    33  			case <-tg.StopChan():
    34  			}
    35  		}()
    36  	}
    37  	start := time.Now()
    38  	err := tg.Stop()
    39  	elapsed := time.Since(start)
    40  	if err != nil {
    41  		t.Fatal(err)
    42  	} else if elapsed > 100*time.Millisecond {
    43  		t.Fatal("Stop did not interrupt goroutines")
    44  	}
    45  }
    46  
    47  // TestThreadGroupWait tests that a thread group will correctly wait for
    48  // existing processes to halt.
    49  func TestThreadGroupWait(t *testing.T) {
    50  	if testing.Short() {
    51  		t.SkipNow()
    52  	}
    53  	t.Parallel()
    54  
    55  	var tg ThreadGroup
    56  	for i := 0; i < 10; i++ {
    57  		err := tg.Add()
    58  		if err != nil {
    59  			t.Fatal(err)
    60  		}
    61  
    62  		go func() {
    63  			defer tg.Done()
    64  			time.Sleep(time.Second)
    65  		}()
    66  	}
    67  	start := time.Now()
    68  	err := tg.Stop()
    69  	elapsed := time.Since(start)
    70  	if err != nil {
    71  		t.Fatal(err)
    72  	} else if elapsed < time.Millisecond*950 {
    73  		t.Fatal("Stop did not wait for goroutines:", elapsed)
    74  	}
    75  }
    76  
    77  // TestThreadGroupStop tests the behavior of a ThreadGroup after Stop has been
    78  // called.
    79  func TestThreadGroupStop(t *testing.T) {
    80  	// Create a thread group and stop it.
    81  	var tg ThreadGroup
    82  	// Create an array to track the order of execution for OnStop and AfterStop
    83  	// calls.
    84  	var stopCalls []int
    85  
    86  	// isStopped should return false
    87  	if tg.isStopped() {
    88  		t.Error("isStopped returns true on unstopped ThreadGroup")
    89  	}
    90  	// The cannel provided by StopChan should be open.
    91  	select {
    92  	case <-tg.StopChan():
    93  		t.Error("stop chan appears to be closed")
    94  	default:
    95  	}
    96  
    97  	// OnStop and AfterStop should queue their functions, but not call them.
    98  	// 'Add' and 'Done' are setup around the OnStop functions, to make sure
    99  	// that the OnStop functions are called before waiting for all calls to
   100  	// 'Done' to come through.
   101  	//
   102  	// Note: the practice of calling Add outside of OnStop and Done inside of
   103  	// OnStop is a bad one - any call to tg.Flush() will cause a deadlock
   104  	// because the stop functions will not be called but tg.Flush will be
   105  	// waiting for the thread group counter to reach zero.
   106  	err := tg.Add()
   107  	if err != nil {
   108  		t.Fatal(err)
   109  	}
   110  	err = tg.Add()
   111  	if err != nil {
   112  		t.Fatal(err)
   113  	}
   114  	tg.OnStop(func() {
   115  		tg.Done()
   116  		stopCalls = append(stopCalls, 1)
   117  	})
   118  	tg.OnStop(func() {
   119  		tg.Done()
   120  		stopCalls = append(stopCalls, 2)
   121  	})
   122  	tg.AfterStop(func() {
   123  		stopCalls = append(stopCalls, 10)
   124  	})
   125  	tg.AfterStop(func() {
   126  		stopCalls = append(stopCalls, 20)
   127  	})
   128  	// None of the stop calls should have been called yet.
   129  	if len(stopCalls) != 0 {
   130  		t.Fatal("Stop calls were called too early")
   131  	}
   132  
   133  	// Stop the thread group.
   134  	err = tg.Stop()
   135  	if err != nil {
   136  		t.Fatal(err)
   137  	}
   138  	// isStopped should return true.
   139  	if !tg.isStopped() {
   140  		t.Error("isStopped returns false on stopped ThreadGroup")
   141  	}
   142  	// The cannel provided by StopChan should be closed.
   143  	select {
   144  	case <-tg.StopChan():
   145  	default:
   146  		t.Error("stop chan appears to be closed")
   147  	}
   148  	// The OnStop calls should have been called first, in reverse order, and
   149  	// the AfterStop calls should have been called second, in reverse order.
   150  	if len(stopCalls) != 4 {
   151  		t.Fatal("Stop did not call the stopping functions correctly")
   152  	}
   153  	if stopCalls[0] != 2 {
   154  		t.Error("Stop called the stopping functions in the wrong order")
   155  	}
   156  	if stopCalls[1] != 1 {
   157  		t.Error("Stop called the stopping functions in the wrong order")
   158  	}
   159  	if stopCalls[2] != 20 {
   160  		t.Error("Stop called the stopping functions in the wrong order")
   161  	}
   162  	if stopCalls[3] != 10 {
   163  		t.Error("Stop called the stopping functions in the wrong order")
   164  	}
   165  
   166  	// Add and Stop should return errors.
   167  	err = tg.Add()
   168  	if err != ErrStopped {
   169  		t.Error("expected ErrStopped, got", err)
   170  	}
   171  	err = tg.Stop()
   172  	if err != ErrStopped {
   173  		t.Error("expected ErrStopped, got", err)
   174  	}
   175  
   176  	// OnStop and AfterStop should call their functions immediately now that
   177  	// the thread group has stopped.
   178  	onStopCalled := false
   179  	tg.OnStop(func() {
   180  		onStopCalled = true
   181  	})
   182  	if !onStopCalled {
   183  		t.Error("OnStop function not called immediately despite the thread group being closed already.")
   184  	}
   185  	afterStopCalled := false
   186  	tg.AfterStop(func() {
   187  		afterStopCalled = true
   188  	})
   189  	if !afterStopCalled {
   190  		t.Error("AfterStop function not called immediately despite the thread group being closed already.")
   191  	}
   192  }
   193  
   194  // TestThreadGroupConcurrentAdd tests that Add can be called concurrently with Stop.
   195  func TestThreadGroupConcurrentAdd(t *testing.T) {
   196  	if testing.Short() {
   197  		t.SkipNow()
   198  	}
   199  	var tg ThreadGroup
   200  	for i := 0; i < 10; i++ {
   201  		go func() {
   202  			err := tg.Add()
   203  			if err != nil {
   204  				return
   205  			}
   206  			defer tg.Done()
   207  
   208  			select {
   209  			case <-time.After(1 * time.Second):
   210  			case <-tg.StopChan():
   211  			}
   212  		}()
   213  	}
   214  	time.Sleep(10 * time.Millisecond) // wait for at least one Add
   215  	err := tg.Stop()
   216  	if err != nil {
   217  		t.Fatal(err)
   218  	}
   219  }
   220  
   221  // TestThreadGroupOnce tests that a zero-valued ThreadGroup's stopChan is
   222  // properly initialized.
   223  func TestThreadGroupOnce(t *testing.T) {
   224  	tg := new(ThreadGroup)
   225  	if tg.stopChan != nil {
   226  		t.Error("expected nil stopChan")
   227  	}
   228  
   229  	// these methods should cause stopChan to be initialized
   230  	tg.StopChan()
   231  	if tg.stopChan == nil {
   232  		t.Error("stopChan should have been initialized by StopChan")
   233  	}
   234  
   235  	tg = new(ThreadGroup)
   236  	tg.isStopped()
   237  	if tg.stopChan == nil {
   238  		t.Error("stopChan should have been initialized by isStopped")
   239  	}
   240  
   241  	tg = new(ThreadGroup)
   242  	tg.Add()
   243  	if tg.stopChan == nil {
   244  		t.Error("stopChan should have been initialized by Add")
   245  	}
   246  
   247  	tg = new(ThreadGroup)
   248  	tg.Stop()
   249  	if tg.stopChan == nil {
   250  		t.Error("stopChan should have been initialized by Stop")
   251  	}
   252  }
   253  
   254  // TestThreadGroupOnStop tests that Stop calls functions registered with
   255  // OnStop.
   256  func TestThreadGroupOnStop(t *testing.T) {
   257  	if testing.Short() {
   258  		t.SkipNow()
   259  	}
   260  	l, err := net.Listen("tcp", "localhost:0")
   261  	if err != nil {
   262  		t.Fatal(err)
   263  	}
   264  
   265  	// create ThreadGroup and register the closer
   266  	var tg ThreadGroup
   267  	tg.OnStop(func() { l.Close() })
   268  
   269  	// send on channel when listener is closed
   270  	var closed bool
   271  	tg.Add()
   272  	go func() {
   273  		defer tg.Done()
   274  		_, err := l.Accept()
   275  		closed = err != nil
   276  	}()
   277  
   278  	tg.Stop()
   279  	if !closed {
   280  		t.Fatal("Stop did not close listener")
   281  	}
   282  }
   283  
   284  // TestThreadGroupRace tests that calling ThreadGroup methods concurrently
   285  // does not trigger the race detector.
   286  func TestThreadGroupRace(t *testing.T) {
   287  	var tg ThreadGroup
   288  	go tg.StopChan()
   289  	go func() {
   290  		if tg.Add() == nil {
   291  			tg.Done()
   292  		}
   293  	}()
   294  	err := tg.Stop()
   295  	if err != nil {
   296  		t.Fatal(err)
   297  	}
   298  }
   299  
   300  // TestThreadGroupCloseAfterStop checks that an AfterStop function is
   301  // correctly called after the thread is stopped.
   302  func TestThreadGroupClosedAfterStop(t *testing.T) {
   303  	var tg ThreadGroup
   304  	var closed bool
   305  	tg.AfterStop(func() { closed = true })
   306  	if closed {
   307  		t.Fatal("close function should not have been called yet")
   308  	}
   309  	if err := tg.Stop(); err != nil {
   310  		t.Fatal(err)
   311  	}
   312  	if !closed {
   313  		t.Fatal("close function should have been called")
   314  	}
   315  
   316  	// Stop has already been called, so the close function should be called
   317  	// immediately
   318  	closed = false
   319  	tg.AfterStop(func() { closed = true })
   320  	if !closed {
   321  		t.Fatal("close function should have been called immediately")
   322  	}
   323  }
   324  
   325  // TestThreadGroupSiaExample tries to use a thread group as it might be
   326  // expected to be used by a module of Sia.
   327  func TestThreadGroupSiaExample(t *testing.T) {
   328  	if testing.Short() {
   329  		t.SkipNow()
   330  	}
   331  	t.Parallel()
   332  	testDir := build.TempDir("sync", t.Name())
   333  	err := os.MkdirAll(testDir, 0700)
   334  	if err != nil {
   335  		t.Fatal(err)
   336  	}
   337  	var tg ThreadGroup
   338  
   339  	// Open an example file. The file is expected to be used throughout the
   340  	// lifetime of the module, and should not be closed until 'AfterStop' is
   341  	// called.
   342  	fileClosed := false
   343  	file, err := os.Create(filepath.Join(testDir, "exampleFile.txt"))
   344  	if err != nil {
   345  		t.Fatal(err)
   346  	}
   347  	tg.AfterStop(func() {
   348  		fileClosed = true
   349  		err := file.Close()
   350  		if err != nil {
   351  			t.Fatal(err)
   352  		}
   353  	})
   354  
   355  	// Open a listener. The listener and handler thread should be closed before
   356  	// the file is closed.
   357  	listenerCleanedUp := false
   358  	listener, err := net.Listen("tcp", "localhost:0")
   359  	if err != nil {
   360  		t.Fatal(err)
   361  	}
   362  	// Open a thread to accept calls from the listener.
   363  	handlerFinishedChan := make(chan struct{})
   364  	go func() {
   365  		for {
   366  			_, err := listener.Accept()
   367  			if err != nil {
   368  				break
   369  			}
   370  		}
   371  		handlerFinishedChan <- struct{}{}
   372  	}()
   373  	tg.OnStop(func() {
   374  		err := listener.Close()
   375  		if err != nil {
   376  			t.Fatal(err)
   377  		}
   378  		<-handlerFinishedChan
   379  
   380  		if fileClosed {
   381  			t.Error("file should be open while the listener is shutting down")
   382  		}
   383  		listenerCleanedUp = true
   384  	})
   385  
   386  	// Create a thread that does some stuff which takes time, and then closes.
   387  	// Use Flush to clear out the process without closing the resources.
   388  	threadFinished := false
   389  	err = tg.Add()
   390  	if err != nil {
   391  		t.Fatal(err)
   392  	}
   393  	go func() {
   394  		time.Sleep(time.Second)
   395  		threadFinished = true
   396  		tg.Done()
   397  	}()
   398  	tg.Flush()
   399  	if !threadFinished {
   400  		t.Error("call to Flush should have allowed the working thread to finish")
   401  	}
   402  	if listenerCleanedUp || fileClosed {
   403  		t.Error("call to Flush resulted in permanent resources being closed")
   404  	}
   405  
   406  	// Create a thread that does some stuff which takes time, and then closes.
   407  	// Use Stop to wait for the threead to finish and then check that all
   408  	// resources have closed.
   409  	threadFinished2 := false
   410  	err = tg.Add()
   411  	if err != nil {
   412  		t.Fatal(err)
   413  	}
   414  	go func() {
   415  		time.Sleep(time.Second)
   416  		threadFinished2 = true
   417  		tg.Done()
   418  	}()
   419  	tg.Stop()
   420  	if !threadFinished2 || !listenerCleanedUp || !fileClosed {
   421  		t.Error("stop did not block until all running resources had closed")
   422  	}
   423  }
   424  
   425  // TestAddOnStop checks that you can safely call OnStop from under the
   426  // protection of an Add call.
   427  func TestAddOnStop(t *testing.T) {
   428  	if testing.Short() {
   429  		t.SkipNow()
   430  	}
   431  	t.Parallel()
   432  
   433  	var tg ThreadGroup
   434  	var data int
   435  	addChan := make(chan struct{})
   436  	stopChan := make(chan struct{})
   437  	tg.OnStop(func() {
   438  		close(stopChan)
   439  	})
   440  	go func() {
   441  		err := tg.Add()
   442  		if err != nil {
   443  			t.Fatal(err)
   444  		}
   445  		close(addChan)
   446  
   447  		// Wait for the call to 'Stop' to be called in the parent thread, and
   448  		// then queue a bunch of 'OnStop' and 'AfterStop' functions before
   449  		// calling 'Done'.
   450  		<-stopChan
   451  		for i := 0; i < 10; i++ {
   452  			tg.OnStop(func() {
   453  				data++
   454  			})
   455  			tg.AfterStop(func() {
   456  				data++
   457  			})
   458  		}
   459  		tg.Done()
   460  	}()
   461  
   462  	// Wait for 'Add' to be called in the above thread, to guarantee that
   463  	// OnStop and AfterStop will be called after 'Add' and 'Stop' have been
   464  	// called together.
   465  	<-addChan
   466  	err := tg.Stop()
   467  	if err != nil {
   468  		t.Fatal(err)
   469  	}
   470  
   471  	if data != 20 {
   472  		t.Error("20 calls were made to increment data, but value is", data)
   473  	}
   474  }
   475  
   476  // BenchmarkThreadGroup times how long it takes to add a ton of threads and
   477  // trigger goroutines that call Done.
   478  func BenchmarkThreadGroup(b *testing.B) {
   479  	var tg ThreadGroup
   480  	for i := 0; i < b.N; i++ {
   481  		tg.Add()
   482  		go tg.Done()
   483  	}
   484  	tg.Stop()
   485  }
   486  
   487  // BenchmarkWaitGroup times how long it takes to add a ton of threads to a wait
   488  // group and trigger goroutines that call Done.
   489  func BenchmarkWaitGroup(b *testing.B) {
   490  	var wg sync.WaitGroup
   491  	for i := 0; i < b.N; i++ {
   492  		wg.Add(1)
   493  		go wg.Done()
   494  	}
   495  	wg.Wait()
   496  }