github.com/grailbio/base@v0.0.11/sync/ctxsync/mutex_test.go (about)

     1  // Copyright 2022 GRAIL, Inc. All rights reserved.
     2  // Use of this source code is governed by the Apache 2.0
     3  // license that can be found in the LICENSE file.
     4  
     5  package ctxsync_test
     6  
     7  import (
     8  	"context"
     9  	"sync"
    10  	"testing"
    11  	"time"
    12  
    13  	"github.com/grailbio/base/errors"
    14  	"github.com/grailbio/base/sync/ctxsync"
    15  	"github.com/stretchr/testify/assert"
    16  	"github.com/stretchr/testify/require"
    17  	"golang.org/x/sync/errgroup"
    18  )
    19  
    20  // TestExclusion verifies that a mutex provides basic mutually exclusive
    21  // access: only one goroutine can have it locked at a time.
    22  func TestExclusion(t *testing.T) {
    23  	var (
    24  		mu ctxsync.Mutex
    25  		wg sync.WaitGroup
    26  		x  int
    27  	)
    28  	require.NoError(t, mu.Lock(context.Background()))
    29  	wg.Add(1)
    30  	go func() {
    31  		defer wg.Done()
    32  		if err := mu.Lock(context.Background()); err != nil {
    33  			return
    34  		}
    35  		x = 100
    36  		mu.Unlock()
    37  	}()
    38  	for i := 1; i <= 10; i++ {
    39  		// Verify that nothing penetrates our lock and changes x unexpectedly.
    40  		assert.Equal(t, i-1, x)
    41  		x = i
    42  		time.Sleep(1 * time.Millisecond)
    43  	}
    44  	mu.Unlock()
    45  	wg.Wait()
    46  	assert.Equal(t, 100, x)
    47  }
    48  
    49  // TestOtherGoroutineUnlock verifies that locked mutexes can be unlocked by a
    50  // different goroutine, and that the lock still provides mutual exclusion
    51  // across them.
    52  func TestOtherGoroutineUnlock(t *testing.T) {
    53  	const N = 100
    54  	var (
    55  		mu       ctxsync.Mutex
    56  		g        errgroup.Group
    57  		chLocked = make(chan struct{})
    58  		x        int
    59  	)
    60  	// Run N goroutines each trying to lock the mutex.  Run another N
    61  	// goroutines, one of which is selected to unlock the mutex after each time
    62  	// it is successfully locked.
    63  	for i := 0; i < N; i++ {
    64  		g.Go(func() error {
    65  			if err := mu.Lock(context.Background()); err != nil {
    66  				return err
    67  			}
    68  			x++
    69  			chLocked <- struct{}{}
    70  			return nil
    71  		})
    72  		g.Go(func() error {
    73  			<-chLocked
    74  			x++
    75  			mu.Unlock()
    76  			return nil
    77  		})
    78  	}
    79  	assert.NoError(t, g.Wait())
    80  	// We run N*2 goroutines, each incrementing x by 1 while the lock is held.
    81  	assert.Equal(t, N*2, x)
    82  }
    83  
    84  // TestCancel verifies that canceling the Lock context causes the attempt to
    85  // lock the mutex to fail and return an error of kind errors.Canceled.
    86  func TestCancel(t *testing.T) {
    87  	var (
    88  		mu        ctxsync.Mutex
    89  		wg        sync.WaitGroup
    90  		errWaiter error
    91  	)
    92  	require.NoError(t, mu.Lock(context.Background()))
    93  	ctx, cancel := context.WithCancel(context.Background())
    94  	wg.Add(1)
    95  	go func() {
    96  		defer wg.Done()
    97  		if errWaiter = mu.Lock(ctx); errWaiter != nil {
    98  			return
    99  		}
   100  		mu.Unlock()
   101  	}()
   102  	cancel()
   103  	wg.Wait()
   104  	mu.Unlock()
   105  	// Verify that we can still lock and unlock after the canceled attempt.
   106  	if assert.NoError(t, mu.Lock(context.Background())) {
   107  		mu.Unlock()
   108  	}
   109  	// Verify that Lock returned the expected non-nil error from the canceled
   110  	// attempt.
   111  	assert.True(t, errors.Is(errors.Canceled, errWaiter), "expected errors.Canceled")
   112  }
   113  
   114  // TestUnlockUnlocked verifies that unlocking a mutex that is not locked
   115  // panics.
   116  func TestUnlockUnlocked(t *testing.T) {
   117  	var mu ctxsync.Mutex
   118  	assert.Panics(t, func() { mu.Unlock() })
   119  }