github.com/Schaudge/grailbase@v0.0.0-20240223061707-44c758a471c0/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/Schaudge/grailbase/errors" 14 "github.com/Schaudge/grailbase/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 }