github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/pkg/lock/lock_test.go (about) 1 // Copyright 2009 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // GOMAXPROCS=10 go test 6 7 package lock 8 9 import ( 10 "flag" 11 "fmt" 12 "runtime" 13 "sync/atomic" 14 "testing" 15 "time" 16 17 "github.com/cozy/cozy-stack/pkg/prefixer" 18 "github.com/redis/go-redis/v9" 19 "github.com/stretchr/testify/assert" 20 "github.com/stretchr/testify/require" 21 ) 22 23 // If you want to test harder the lock, you can set nb = 1000 but it is too 24 // slow for CI, and the lock package has very few commits in the last years. 25 var nb = 100 26 27 func TestLock(t *testing.T) { 28 if testing.Short() { 29 t.Skip("an instance is required for this test: test skipped due to the use of --short flag") 30 } 31 32 flag.Parse() 33 if testing.Short() { 34 nb = 3 35 } 36 37 t.Run("MemLock", func(t *testing.T) { 38 client := NewInMemory() 39 40 db := prefixer.NewPrefixer(0, "cozy.local", "cozy.local") 41 l := client.ReadWrite(db, "test-mem") 42 hammerRW(t, l) 43 }) 44 45 t.Run("RedisLock", func(t *testing.T) { 46 opt, err := redis.ParseURL("redis://localhost:6379/0") 47 require.NoError(t, err) 48 client := NewRedisLockGetter(redis.NewClient(opt)) 49 50 db := prefixer.NewPrefixer(0, "cozy.local", "cozy.local") 51 l := client.ReadWrite(db, "test-redis") 52 l.(*redisLock).timeout = time.Second 53 l.(*redisLock).waitRetry = 100 * time.Millisecond 54 55 require.NoError(t, l.RLock()) 56 require.NoError(t, l.RLock()) 57 l.RUnlock() 58 require.Error(t, l.Lock()) 59 l.RUnlock() 60 require.NoError(t, l.Lock()) 61 l.Unlock() 62 63 hammerRW(t, l) 64 65 done := make(chan bool) 66 for i := 0; i < 10; i++ { 67 go HammerMutex(l, done) 68 } 69 for i := 0; i < 10; i++ { 70 <-done 71 } 72 73 other := client.ReadWrite(db, "test-redis") 74 assert.NoError(t, l.Lock()) 75 assert.Error(t, other.Lock()) 76 77 l.Unlock() 78 }) 79 80 t.Run("LongLock", func(t *testing.T) { 81 if testing.Short() { 82 return 83 } 84 85 opt, err := redis.ParseURL("redis://localhost:6379/0") 86 require.NoError(t, err) 87 client := NewRedisLockGetter(redis.NewClient(opt)) 88 89 db := prefixer.NewPrefixer(0, "cozy.local", "cozy.local") 90 long := client.LongOperation(db, "test-long") 91 92 // Reduce the default timeout duration. 93 long.(*longOperation).timeout = 50 * time.Millisecond 94 95 l := client.ReadWrite(db, "test-long") 96 l.(*redisLock).timeout = 200 * time.Millisecond 97 l.(*redisLock).waitRetry = 10 * time.Millisecond 98 99 // Take the lock and refresh it every 20ms without 100 // losing the lock 101 assert.NoError(t, long.Lock()) 102 103 // Try a second lock. It should fail after 200ms, and the long lock 104 // will have been extended a few times in this span of time. 105 err = l.Lock() 106 assert.Error(t, err) 107 assert.Equal(t, ErrTooManyRetries, err) 108 long.Unlock() 109 110 // Check that a long lock can be reused 111 assert.NoError(t, long.Lock()) 112 long.Unlock() 113 }) 114 } 115 116 func reader(rwm ErrorRWLocker, iterations int, activity *int32, cdone chan bool) { 117 for i := 0; i < iterations; i++ { 118 err := rwm.RLock() 119 if err != nil { 120 panic(err) 121 } 122 n := atomic.AddInt32(activity, 1) 123 if n < 1 || n >= 10000 { 124 panic(fmt.Sprintf("wlock(%d)\n", n)) 125 } 126 for i := 0; i < 100; i++ { 127 } 128 atomic.AddInt32(activity, -1) 129 rwm.RUnlock() 130 } 131 cdone <- true 132 } 133 134 func writer(rwm ErrorRWLocker, iterations int, activity *int32, cdone chan bool) { 135 for i := 0; i < iterations; i++ { 136 err := rwm.Lock() 137 if err != nil { 138 panic(err) 139 } 140 n := atomic.AddInt32(activity, 10000) 141 if n != 10000 { 142 panic(fmt.Sprintf("wlock(%d)\n", n)) 143 } 144 for i := 0; i < 100; i++ { 145 } 146 atomic.AddInt32(activity, -10000) 147 rwm.Unlock() 148 } 149 cdone <- true 150 } 151 152 func HammerRWMutex(locker ErrorRWLocker, gomaxprocs, numReaders, iterations int) { 153 runtime.GOMAXPROCS(gomaxprocs) 154 // Number of active readers + 10000 * number of active writers. 155 var activity int32 156 cdone := make(chan bool) 157 go writer(locker, iterations, &activity, cdone) 158 var i int 159 for i = 0; i < numReaders/2; i++ { 160 go reader(locker, iterations, &activity, cdone) 161 } 162 go writer(locker, iterations, &activity, cdone) 163 for ; i < numReaders; i++ { 164 go reader(locker, iterations, &activity, cdone) 165 } 166 // Wait for the 2 writers and all readers to finish. 167 for i := 0; i < 2+numReaders; i++ { 168 <-cdone 169 } 170 } 171 172 func HammerMutex(m ErrorLocker, cdone chan bool) { 173 for i := 0; i < nb; i++ { 174 err := m.Lock() 175 if err != nil { 176 panic(err) 177 } 178 m.Unlock() 179 } 180 cdone <- true 181 } 182 183 func hammerRW(t *testing.T, l ErrorRWLocker) { 184 defer runtime.GOMAXPROCS(runtime.GOMAXPROCS(-1)) 185 HammerRWMutex(l, 1, 1, nb) 186 HammerRWMutex(l, 1, 3, nb) 187 HammerRWMutex(l, 1, 10, nb) 188 HammerRWMutex(l, 4, 1, nb) 189 HammerRWMutex(l, 4, 3, nb) 190 HammerRWMutex(l, 4, 10, nb) 191 HammerRWMutex(l, 10, 1, nb) 192 HammerRWMutex(l, 10, 3, nb) 193 HammerRWMutex(l, 10, 10, nb) 194 HammerRWMutex(l, 10, 5, nb) 195 }