github.com/ericjee/storage@v1.12.13/lockfile_test.go (about)

     1  package storage
     2  
     3  import (
     4  	"fmt"
     5  	"io"
     6  	"io/ioutil"
     7  	"os"
     8  	"sync"
     9  	"sync/atomic"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/containers/storage/pkg/reexec"
    14  	"github.com/sirupsen/logrus"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  )
    18  
    19  // Warning: this is not an exhaustive set of tests.
    20  
    21  func TestMain(m *testing.M) {
    22  	if reexec.Init() {
    23  		return
    24  	}
    25  	os.Exit(m.Run())
    26  }
    27  
    28  // subTouchMain is a child process which opens the lock file, closes stdout to
    29  // indicate that it has acquired the lock, waits for stdin to get closed,
    30  // updates the last-writer for the lockfile, and then unlocks the file.
    31  func subTouchMain() {
    32  	if len(os.Args) != 2 {
    33  		logrus.Fatalf("expected two args, got %d", len(os.Args))
    34  	}
    35  	tf, err := GetLockfile(os.Args[1])
    36  	if err != nil {
    37  		logrus.Fatalf("error opening lock file %q: %v", os.Args[1], err)
    38  	}
    39  	tf.Lock()
    40  	os.Stdout.Close()
    41  	io.Copy(ioutil.Discard, os.Stdin)
    42  	tf.Touch()
    43  	tf.Unlock()
    44  }
    45  
    46  // subTouch starts a child process.  If it doesn't return an error, the caller
    47  // should wait for the first ReadCloser by reading it until it receives an EOF.
    48  // At that point, the child will have acquired the lock.  It can then signal
    49  // that the child should Touch() the lock by closing the WriteCloser.  The
    50  // second ReadCloser will be closed when the child has finished.
    51  func subTouch(l *namedLocker) (io.WriteCloser, io.ReadCloser, io.ReadCloser, error) {
    52  	cmd := reexec.Command("subTouch", l.name)
    53  	wc, err := cmd.StdinPipe()
    54  	if err != nil {
    55  		return nil, nil, nil, err
    56  	}
    57  	rc, err := cmd.StdoutPipe()
    58  	if err != nil {
    59  		return nil, nil, nil, err
    60  	}
    61  	ec, err := cmd.StderrPipe()
    62  	if err != nil {
    63  		return nil, nil, nil, err
    64  	}
    65  	go func() {
    66  		if err = cmd.Run(); err != nil {
    67  			logrus.Errorf("error running subTouch: %v", err)
    68  		}
    69  	}()
    70  	return wc, rc, ec, nil
    71  }
    72  
    73  // subLockMain is a child process which opens the lock file, closes stdout to
    74  // indicate that it has acquired the lock, waits for stdin to get closed, and
    75  // then unlocks the file.
    76  func subLockMain() {
    77  	if len(os.Args) != 2 {
    78  		logrus.Fatalf("expected two args, got %d", len(os.Args))
    79  	}
    80  	tf, err := GetLockfile(os.Args[1])
    81  	if err != nil {
    82  		logrus.Fatalf("error opening lock file %q: %v", os.Args[1], err)
    83  	}
    84  	tf.Lock()
    85  	os.Stdout.Close()
    86  	io.Copy(ioutil.Discard, os.Stdin)
    87  	tf.Unlock()
    88  }
    89  
    90  // subLock starts a child process.  If it doesn't return an error, the caller
    91  // should wait for the first ReadCloser by reading it until it receives an EOF.
    92  // At that point, the child will have acquired the lock.  It can then signal
    93  // that the child should release the lock by closing the WriteCloser.
    94  func subLock(l *namedLocker) (io.WriteCloser, io.ReadCloser, error) {
    95  	cmd := reexec.Command("subLock", l.name)
    96  	wc, err := cmd.StdinPipe()
    97  	if err != nil {
    98  		return nil, nil, err
    99  	}
   100  	rc, err := cmd.StdoutPipe()
   101  	if err != nil {
   102  		return nil, nil, err
   103  	}
   104  	go func() {
   105  		if err = cmd.Run(); err != nil {
   106  			logrus.Errorf("error running subLock: %v", err)
   107  		}
   108  	}()
   109  	return wc, rc, nil
   110  }
   111  
   112  // subRecursiveLockMain is a child process which opens the lock file, closes
   113  // stdout to indicate that it has acquired the lock, waits for stdin to get
   114  // closed, and then unlocks the file.
   115  func subRecursiveLockMain() {
   116  	if len(os.Args) != 2 {
   117  		logrus.Fatalf("expected two args, got %d", len(os.Args))
   118  	}
   119  	tf, err := GetLockfile(os.Args[1])
   120  	if err != nil {
   121  		logrus.Fatalf("error opening lock file %q: %v", os.Args[1], err)
   122  	}
   123  	tf.RecursiveLock()
   124  	os.Stdout.Close()
   125  	io.Copy(ioutil.Discard, os.Stdin)
   126  	tf.Unlock()
   127  }
   128  
   129  // subRecursiveLock starts a child process.  If it doesn't return an error, the
   130  // caller should wait for the first ReadCloser by reading it until it receives
   131  // an EOF. At that point, the child will have acquired the lock.  It can then
   132  // signal that the child should release the lock by closing the WriteCloser.
   133  func subRecursiveLock(l *namedLocker) (io.WriteCloser, io.ReadCloser, error) {
   134  	cmd := reexec.Command("subRecursiveLock", l.name)
   135  	wc, err := cmd.StdinPipe()
   136  	if err != nil {
   137  		return nil, nil, err
   138  	}
   139  	rc, err := cmd.StdoutPipe()
   140  	if err != nil {
   141  		return nil, nil, err
   142  	}
   143  	go func() {
   144  		if err = cmd.Run(); err != nil {
   145  			logrus.Errorf("error running subLock: %v", err)
   146  		}
   147  	}()
   148  	return wc, rc, nil
   149  }
   150  
   151  // subRLockMain is a child process which opens the lock file, closes stdout to
   152  // indicate that it has acquired the read lock, waits for stdin to get closed,
   153  // and then unlocks the file.
   154  func subRLockMain() {
   155  	if len(os.Args) != 2 {
   156  		logrus.Fatalf("expected two args, got %d", len(os.Args))
   157  	}
   158  	tf, err := GetLockfile(os.Args[1])
   159  	if err != nil {
   160  		logrus.Fatalf("error opening lock file %q: %v", os.Args[1], err)
   161  	}
   162  	tf.RLock()
   163  	os.Stdout.Close()
   164  	io.Copy(ioutil.Discard, os.Stdin)
   165  	tf.Unlock()
   166  }
   167  
   168  // subRLock starts a child process.  If it doesn't return an error, the caller
   169  // should wait for the first ReadCloser by reading it until it receives an EOF.
   170  // At that point, the child will have acquired a read lock.  It can then signal
   171  // that the child should release the lock by closing the WriteCloser.
   172  func subRLock(l *namedLocker) (io.WriteCloser, io.ReadCloser, error) {
   173  	cmd := reexec.Command("subRLock", l.name)
   174  	wc, err := cmd.StdinPipe()
   175  	if err != nil {
   176  		return nil, nil, err
   177  	}
   178  	rc, err := cmd.StdoutPipe()
   179  	if err != nil {
   180  		return nil, nil, err
   181  	}
   182  	go func() {
   183  		if err = cmd.Run(); err != nil {
   184  			logrus.Errorf("error running subRLock: %v", err)
   185  		}
   186  	}()
   187  	return wc, rc, nil
   188  }
   189  
   190  func init() {
   191  	reexec.Register("subTouch", subTouchMain)
   192  	reexec.Register("subRLock", subRLockMain)
   193  	reexec.Register("subRecursiveLock", subRecursiveLockMain)
   194  	reexec.Register("subLock", subLockMain)
   195  }
   196  
   197  type namedLocker struct {
   198  	Locker
   199  	name string
   200  }
   201  
   202  func getNamedLocker(ro bool) (*namedLocker, error) {
   203  	var l Locker
   204  	tf, err := ioutil.TempFile("", "lockfile")
   205  	if err != nil {
   206  		return nil, err
   207  	}
   208  	name := tf.Name()
   209  	tf.Close()
   210  	if ro {
   211  		l, err = GetROLockfile(name)
   212  	} else {
   213  		l, err = GetLockfile(name)
   214  	}
   215  	if err != nil {
   216  		return nil, err
   217  	}
   218  	return &namedLocker{Locker: l, name: name}, nil
   219  }
   220  
   221  func getTempLockfile() (*namedLocker, error) {
   222  	return getNamedLocker(false)
   223  }
   224  
   225  func getTempROLockfile() (*namedLocker, error) {
   226  	return getNamedLocker(true)
   227  }
   228  
   229  func TestLockfileName(t *testing.T) {
   230  	l, err := getTempLockfile()
   231  	require.Nil(t, err, "error getting temporary lock file")
   232  	defer os.Remove(l.name)
   233  
   234  	assert.NotEmpty(t, l.name, "lockfile name should be recorded correctly")
   235  
   236  	assert.False(t, l.Locked(), "Locked() said we have a write lock")
   237  
   238  	l.RLock()
   239  	assert.False(t, l.Locked(), "Locked() said we have a write lock")
   240  	l.Unlock()
   241  
   242  	assert.NotEmpty(t, l.name, "lockfile name should be recorded correctly")
   243  
   244  	l.Lock()
   245  	assert.True(t, l.Locked(), "Locked() said we didn't have a write lock")
   246  	l.Unlock()
   247  
   248  	assert.NotEmpty(t, l.name, "lockfile name should be recorded correctly")
   249  }
   250  
   251  func TestLockfileRead(t *testing.T) {
   252  	l, err := getTempLockfile()
   253  	require.Nil(t, err, "error getting temporary lock file")
   254  	defer os.Remove(l.name)
   255  
   256  	l.RLock()
   257  	assert.False(t, l.Locked(), "Locked() said we have a write lock")
   258  	l.Unlock()
   259  }
   260  
   261  func TestROLockfileRead(t *testing.T) {
   262  	l, err := getTempROLockfile()
   263  	require.Nil(t, err, "error getting temporary lock file")
   264  	defer os.Remove(l.name)
   265  
   266  	l.Lock()
   267  	assert.False(t, l.Locked(), "Locked() said we have a write lock")
   268  	l.Unlock()
   269  
   270  	l.RLock()
   271  	assert.False(t, l.Locked(), "Locked() said we have a write lock")
   272  	l.Unlock()
   273  }
   274  
   275  func TestLockfileWrite(t *testing.T) {
   276  	l, err := getTempLockfile()
   277  	require.Nil(t, err, "error getting temporary lock file")
   278  	defer os.Remove(l.name)
   279  
   280  	l.Lock()
   281  	assert.True(t, l.Locked(), "Locked() said we didn't have a write lock")
   282  	l.Unlock()
   283  }
   284  
   285  func TestRecursiveLockfileWrite(t *testing.T) {
   286  	l, err := getTempLockfile()
   287  	require.Nil(t, err, "error getting temporary lock file")
   288  	defer os.Remove(l.name)
   289  
   290  	l.RecursiveLock()
   291  	assert.True(t, l.Locked(), "Locked() said we didn't have a write lock")
   292  	l.RecursiveLock()
   293  	l.Unlock()
   294  	l.Unlock()
   295  }
   296  
   297  func TestROLockfileWrite(t *testing.T) {
   298  	l, err := getTempROLockfile()
   299  	require.Nil(t, err, "error getting temporary lock file")
   300  	defer os.Remove(l.name)
   301  
   302  	l.Lock()
   303  	assert.False(t, l.Locked(), "Locked() said we have a write lock")
   304  	l.Unlock()
   305  }
   306  
   307  func TestLockfileTouch(t *testing.T) {
   308  	l, err := getTempLockfile()
   309  	require.Nil(t, err, "error getting temporary lock file")
   310  	defer os.Remove(l.name)
   311  
   312  	l.Lock()
   313  	m, err := l.Modified()
   314  	require.Nil(t, err, "got an error from Modified()")
   315  	assert.True(t, m, "new lock file does not appear to have changed")
   316  
   317  	now := time.Now()
   318  	assert.False(t, l.TouchedSince(now), "file timestamp was updated for no reason")
   319  
   320  	time.Sleep(2 * time.Second)
   321  	err = l.Touch()
   322  	require.Nil(t, err, "got an error from Touch()")
   323  	assert.True(t, l.TouchedSince(now), "file timestamp was not updated by Touch()")
   324  
   325  	m, err = l.Modified()
   326  	require.Nil(t, err, "got an error from Modified()")
   327  	assert.False(t, m, "lock file mistakenly indicated that someone else has modified it")
   328  
   329  	stdin, stdout, stderr, err := subTouch(l)
   330  	require.Nil(t, err, "got an error starting a subprocess to touch the lockfile")
   331  	l.Unlock()
   332  	io.Copy(ioutil.Discard, stdout)
   333  	stdin.Close()
   334  	io.Copy(ioutil.Discard, stderr)
   335  	l.Lock()
   336  	m, err = l.Modified()
   337  	l.Unlock()
   338  	require.Nil(t, err, "got an error from Modified()")
   339  	assert.True(t, m, "lock file failed to notice that someone else modified it")
   340  }
   341  
   342  func TestLockfileWriteConcurrent(t *testing.T) {
   343  	l, err := getTempLockfile()
   344  	require.Nil(t, err, "error getting temporary lock file")
   345  	defer os.Remove(l.name)
   346  	var wg sync.WaitGroup
   347  	var highestMutex sync.Mutex
   348  	var counter, highest int64
   349  	for i := 0; i < 100000; i++ {
   350  		wg.Add(1)
   351  		go func() {
   352  			l.Lock()
   353  			tmp := atomic.AddInt64(&counter, 1)
   354  			assert.True(t, tmp >= 0, "counter should never be less than zero")
   355  			highestMutex.Lock()
   356  			if tmp > highest {
   357  				// multiple writers should not be able to hold
   358  				// this lock at the same time, so there should
   359  				// be no point at which two goroutines are
   360  				// between the AddInt64() above and the one
   361  				// below
   362  				highest = tmp
   363  			}
   364  			highestMutex.Unlock()
   365  			atomic.AddInt64(&counter, -1)
   366  			l.Unlock()
   367  			wg.Done()
   368  		}()
   369  	}
   370  	wg.Wait()
   371  	assert.True(t, highest == 1, "counter should never have gone above 1, got to %d", highest)
   372  }
   373  
   374  func TestLockfileReadConcurrent(t *testing.T) {
   375  	l, err := getTempLockfile()
   376  	require.Nil(t, err, "error getting temporary lock file")
   377  	defer os.Remove(l.name)
   378  
   379  	// the test below is inspired by the stdlib's rwmutex tests
   380  	numReaders := 1000
   381  	locked := make(chan bool)
   382  	unlocked := make(chan bool)
   383  	done := make(chan bool)
   384  
   385  	for i := 0; i < numReaders; i++ {
   386  		go func() {
   387  			l.RLock()
   388  			locked <- true
   389  			<-unlocked
   390  			l.Unlock()
   391  			done <- true
   392  		}()
   393  	}
   394  
   395  	// Wait for all parallel locks to succeed
   396  	for i := 0; i < numReaders; i++ {
   397  		<-locked
   398  	}
   399  	// Instruct all parallel locks to unlock
   400  	for i := 0; i < numReaders; i++ {
   401  		unlocked <- true
   402  	}
   403  	// Wait for all parallel locks to be unlocked
   404  	for i := 0; i < numReaders; i++ {
   405  		<-done
   406  	}
   407  }
   408  
   409  func TestLockfileRecursiveWrite(t *testing.T) {
   410  	// NOTE: given we're in the same process space, it's effectively the same as
   411  	// reader lock.
   412  
   413  	l, err := getTempLockfile()
   414  	require.Nil(t, err, "error getting temporary lock file")
   415  	defer os.Remove(l.name)
   416  
   417  	// the test below is inspired by the stdlib's rwmutex tests
   418  	numReaders := 1000
   419  	locked := make(chan bool)
   420  	unlocked := make(chan bool)
   421  	done := make(chan bool)
   422  
   423  	for i := 0; i < numReaders; i++ {
   424  		go func() {
   425  			l.RecursiveLock()
   426  			locked <- true
   427  			<-unlocked
   428  			l.Unlock()
   429  			done <- true
   430  		}()
   431  	}
   432  
   433  	// Wait for all parallel locks to succeed
   434  	for i := 0; i < numReaders; i++ {
   435  		<-locked
   436  	}
   437  	// Instruct all parallel locks to unlock
   438  	for i := 0; i < numReaders; i++ {
   439  		unlocked <- true
   440  	}
   441  	// Wait for all parallel locks to be unlocked
   442  	for i := 0; i < numReaders; i++ {
   443  		<-done
   444  	}
   445  }
   446  
   447  func TestLockfileMixedConcurrent(t *testing.T) {
   448  	l, err := getTempLockfile()
   449  	require.Nil(t, err, "error getting temporary lock file")
   450  	defer os.Remove(l.name)
   451  
   452  	counter := int32(0)
   453  	diff := int32(10000)
   454  	numIterations := 10
   455  	numReaders := 100
   456  	numWriters := 50
   457  
   458  	done := make(chan bool)
   459  
   460  	// A writer always adds `diff` to the counter. Hence, `diff` is the
   461  	// only valid value in the critical section.
   462  	writer := func(c *int32) {
   463  		for i := 0; i < numIterations; i++ {
   464  			l.Lock()
   465  			tmp := atomic.AddInt32(c, diff)
   466  			assert.True(t, tmp == diff, "counter should be %d but instead is %d", diff, tmp)
   467  			time.Sleep(100 * time.Millisecond)
   468  			atomic.AddInt32(c, diff*(-1))
   469  			l.Unlock()
   470  		}
   471  		done <- true
   472  	}
   473  
   474  	// A reader always adds `1` to the counter. Hence,
   475  	// [1,`numReaders*numIterations`] are valid values.
   476  	reader := func(c *int32) {
   477  		for i := 0; i < numIterations; i++ {
   478  			l.RLock()
   479  			tmp := atomic.AddInt32(c, 1)
   480  			assert.True(t, tmp >= 1 && tmp < diff)
   481  			time.Sleep(100 * time.Millisecond)
   482  			atomic.AddInt32(c, -1)
   483  			l.Unlock()
   484  		}
   485  		done <- true
   486  	}
   487  
   488  	for i := 0; i < numReaders; i++ {
   489  		go reader(&counter)
   490  		// schedule a writer every 2nd iteration
   491  		if i%2 == 1 {
   492  			go writer(&counter)
   493  		}
   494  	}
   495  
   496  	for i := 0; i < numReaders+numWriters; i++ {
   497  		<-done
   498  	}
   499  }
   500  
   501  func TestLockfileMixedConcurrentRecursiveWriters(t *testing.T) {
   502  	// It's effectively the same tests as with mixed readers & writers but calling
   503  	// RecursiveLocks() instead.
   504  
   505  	l, err := getTempLockfile()
   506  	require.Nil(t, err, "error getting temporary lock file")
   507  	defer os.Remove(l.name)
   508  
   509  	counter := int32(0)
   510  	diff := int32(10000)
   511  	numIterations := 10
   512  	numReaders := 100
   513  	numWriters := 50
   514  
   515  	done := make(chan bool)
   516  
   517  	// A writer always adds `diff` to the counter. Hence, `diff` is the
   518  	// only valid value in the critical section.
   519  	writer := func(c *int32) {
   520  		for i := 0; i < numIterations; i++ {
   521  			l.Lock()
   522  			tmp := atomic.AddInt32(c, diff)
   523  			assert.True(t, tmp == diff, "counter should be %d but instead is %d", diff, tmp)
   524  			time.Sleep(100 * time.Millisecond)
   525  			atomic.AddInt32(c, diff*(-1))
   526  			l.Unlock()
   527  		}
   528  		done <- true
   529  	}
   530  
   531  	// A reader always adds `1` to the counter. Hence,
   532  	// [1,`numReaders*numIterations`] are valid values.
   533  	reader := func(c *int32) {
   534  		for i := 0; i < numIterations; i++ {
   535  			l.RecursiveLock()
   536  			tmp := atomic.AddInt32(c, 1)
   537  			assert.True(t, tmp >= 1 && tmp < diff)
   538  			time.Sleep(100 * time.Millisecond)
   539  			atomic.AddInt32(c, -1)
   540  			l.Unlock()
   541  		}
   542  		done <- true
   543  	}
   544  
   545  	for i := 0; i < numReaders; i++ {
   546  		go reader(&counter)
   547  		// schedule a writer every 2nd iteration
   548  		if i%2 == 1 {
   549  			go writer(&counter)
   550  		}
   551  	}
   552  
   553  	for i := 0; i < numReaders+numWriters; i++ {
   554  		<-done
   555  	}
   556  }
   557  
   558  func TestLockfileMultiprocessRead(t *testing.T) {
   559  	l, err := getTempLockfile()
   560  	require.Nil(t, err, "error getting temporary lock file")
   561  	defer os.Remove(l.name)
   562  	var wg sync.WaitGroup
   563  	var rcounter, rhighest int64
   564  	var highestMutex sync.Mutex
   565  	subs := make([]struct {
   566  		stdin  io.WriteCloser
   567  		stdout io.ReadCloser
   568  	}, 100)
   569  	for i := range subs {
   570  		stdin, stdout, err := subRLock(l)
   571  		require.Nil(t, err, "error starting subprocess %d to take a read lock", i+1)
   572  		subs[i].stdin = stdin
   573  		subs[i].stdout = stdout
   574  	}
   575  	for i := range subs {
   576  		wg.Add(1)
   577  		go func(i int) {
   578  			io.Copy(ioutil.Discard, subs[i].stdout)
   579  			if testing.Verbose() {
   580  				fmt.Printf("\tchild %4d acquired the read lock\n", i+1)
   581  			}
   582  			atomic.AddInt64(&rcounter, 1)
   583  			highestMutex.Lock()
   584  			if rcounter > rhighest {
   585  				rhighest = rcounter
   586  			}
   587  			highestMutex.Unlock()
   588  			time.Sleep(1 * time.Second)
   589  			atomic.AddInt64(&rcounter, -1)
   590  			if testing.Verbose() {
   591  				fmt.Printf("\ttelling child %4d to release the read lock\n", i+1)
   592  			}
   593  			subs[i].stdin.Close()
   594  			wg.Done()
   595  		}(i)
   596  	}
   597  	wg.Wait()
   598  	assert.True(t, rhighest > 1, "expected to have multiple reader locks at least once, only had %d", rhighest)
   599  }
   600  
   601  func TestLockfileMultiprocessWrite(t *testing.T) {
   602  	l, err := getTempLockfile()
   603  	require.Nil(t, err, "error getting temporary lock file")
   604  	defer os.Remove(l.name)
   605  	var wg sync.WaitGroup
   606  	var wcounter, whighest int64
   607  	var highestMutex sync.Mutex
   608  	subs := make([]struct {
   609  		stdin  io.WriteCloser
   610  		stdout io.ReadCloser
   611  	}, 10)
   612  	for i := range subs {
   613  		stdin, stdout, err := subLock(l)
   614  		require.Nil(t, err, "error starting subprocess %d to take a write lock", i+1)
   615  		subs[i].stdin = stdin
   616  		subs[i].stdout = stdout
   617  	}
   618  	for i := range subs {
   619  		wg.Add(1)
   620  		go func(i int) {
   621  			io.Copy(ioutil.Discard, subs[i].stdout)
   622  			if testing.Verbose() {
   623  				fmt.Printf("\tchild %4d acquired the write lock\n", i+1)
   624  			}
   625  			atomic.AddInt64(&wcounter, 1)
   626  			highestMutex.Lock()
   627  			if wcounter > whighest {
   628  				whighest = wcounter
   629  			}
   630  			highestMutex.Unlock()
   631  			time.Sleep(1 * time.Second)
   632  			atomic.AddInt64(&wcounter, -1)
   633  			if testing.Verbose() {
   634  				fmt.Printf("\ttelling child %4d to release the write lock\n", i+1)
   635  			}
   636  			subs[i].stdin.Close()
   637  			wg.Done()
   638  		}(i)
   639  	}
   640  	wg.Wait()
   641  	assert.True(t, whighest == 1, "expected to have no more than one writer lock active at a time, had %d", whighest)
   642  }
   643  
   644  func TestLockfileMultiprocessRecursiveWrite(t *testing.T) {
   645  	l, err := getTempLockfile()
   646  	require.Nil(t, err, "error getting temporary lock file")
   647  	defer os.Remove(l.name)
   648  	var wg sync.WaitGroup
   649  	var wcounter, whighest int64
   650  	var highestMutex sync.Mutex
   651  	subs := make([]struct {
   652  		stdin  io.WriteCloser
   653  		stdout io.ReadCloser
   654  	}, 10)
   655  	for i := range subs {
   656  		stdin, stdout, err := subRecursiveLock(l)
   657  		require.Nil(t, err, "error starting subprocess %d to take a write lock", i+1)
   658  		subs[i].stdin = stdin
   659  		subs[i].stdout = stdout
   660  	}
   661  	for i := range subs {
   662  		wg.Add(1)
   663  		go func(i int) {
   664  			io.Copy(ioutil.Discard, subs[i].stdout)
   665  			if testing.Verbose() {
   666  				fmt.Printf("\tchild %4d acquired the recursive write lock\n", i+1)
   667  			}
   668  			atomic.AddInt64(&wcounter, 1)
   669  			highestMutex.Lock()
   670  			if wcounter > whighest {
   671  				whighest = wcounter
   672  			}
   673  			highestMutex.Unlock()
   674  			time.Sleep(1 * time.Second)
   675  			atomic.AddInt64(&wcounter, -1)
   676  			if testing.Verbose() {
   677  				fmt.Printf("\ttelling child %4d to release the recursive write lock\n", i+1)
   678  			}
   679  			subs[i].stdin.Close()
   680  			wg.Done()
   681  		}(i)
   682  	}
   683  	wg.Wait()
   684  	assert.True(t, whighest == 1, "expected to have no more than one writer lock active at a time, had %d", whighest)
   685  }
   686  
   687  func TestLockfileMultiprocessMixed(t *testing.T) {
   688  	l, err := getTempLockfile()
   689  	require.Nil(t, err, "error getting temporary lock file")
   690  	defer os.Remove(l.name)
   691  	var wg sync.WaitGroup
   692  	var rcounter, wcounter, rhighest, whighest int64
   693  	var rhighestMutex, whighestMutex sync.Mutex
   694  	bias_p := 1
   695  	bias_q := 10
   696  	groups := 15
   697  	writer := func(i int) bool { return (i % bias_q) < bias_p }
   698  	subs := make([]struct {
   699  		stdin  io.WriteCloser
   700  		stdout io.ReadCloser
   701  	}, bias_q*groups)
   702  	for i := range subs {
   703  		var stdin io.WriteCloser
   704  		var stdout io.ReadCloser
   705  		if writer(i) {
   706  			stdin, stdout, err = subLock(l)
   707  			require.Nil(t, err, "error starting subprocess %d to take a write lock", i+1)
   708  		} else {
   709  			stdin, stdout, err = subRLock(l)
   710  			require.Nil(t, err, "error starting subprocess %d to take a read lock", i+1)
   711  		}
   712  		subs[i].stdin = stdin
   713  		subs[i].stdout = stdout
   714  	}
   715  	for i := range subs {
   716  		wg.Add(1)
   717  		go func(i int) {
   718  			// wait for the child to acquire whatever lock it wants
   719  			io.Copy(ioutil.Discard, subs[i].stdout)
   720  			if writer(i) {
   721  				// child acquired a write lock
   722  				if testing.Verbose() {
   723  					fmt.Printf("\tchild %4d acquired the write lock\n", i+1)
   724  				}
   725  				atomic.AddInt64(&wcounter, 1)
   726  				whighestMutex.Lock()
   727  				if wcounter > whighest {
   728  					whighest = wcounter
   729  				}
   730  				require.Zero(t, rcounter, "acquired a write lock while we appear to have read locks")
   731  				whighestMutex.Unlock()
   732  			} else {
   733  				// child acquired a read lock
   734  				if testing.Verbose() {
   735  					fmt.Printf("\tchild %4d acquired the read lock\n", i+1)
   736  				}
   737  				atomic.AddInt64(&rcounter, 1)
   738  				rhighestMutex.Lock()
   739  				if rcounter > rhighest {
   740  					rhighest = rcounter
   741  				}
   742  				require.Zero(t, wcounter, "acquired a read lock while we appear to have write locks")
   743  				rhighestMutex.Unlock()
   744  			}
   745  			time.Sleep(1 * time.Second)
   746  			if writer(i) {
   747  				atomic.AddInt64(&wcounter, -1)
   748  				if testing.Verbose() {
   749  					fmt.Printf("\ttelling child %4d to release the write lock\n", i+1)
   750  				}
   751  			} else {
   752  				atomic.AddInt64(&rcounter, -1)
   753  				if testing.Verbose() {
   754  					fmt.Printf("\ttelling child %4d to release the read lock\n", i+1)
   755  				}
   756  			}
   757  			subs[i].stdin.Close()
   758  			wg.Done()
   759  		}(i)
   760  	}
   761  	wg.Wait()
   762  	assert.True(t, rhighest > 1, "expected to have more than one reader lock active at a time at least once, only had %d", rhighest)
   763  	assert.True(t, whighest == 1, "expected to have no more than one writer lock active at a time, had %d", whighest)
   764  }