github.com/mattermost/mattermost-plugin-api@v0.1.4/cluster/mutex_test.go (about) 1 package cluster 2 3 import ( 4 "context" 5 "testing" 6 "time" 7 8 "github.com/mattermost/mattermost-server/v6/model" 9 "github.com/stretchr/testify/assert" 10 "github.com/stretchr/testify/require" 11 ) 12 13 func mustNewMutex(pluginAPI MutexPluginAPI, key string) *Mutex { 14 m, err := NewMutex(pluginAPI, key) 15 if err != nil { 16 panic(err) 17 } 18 19 return m 20 } 21 22 func TestMakeLockKey(t *testing.T) { 23 t.Run("fails when empty", func(t *testing.T) { 24 key, err := makeLockKey("") 25 assert.Error(t, err) 26 assert.Empty(t, key) 27 }) 28 29 t.Run("not-empty", func(t *testing.T) { 30 testCases := map[string]string{ 31 "key": mutexPrefix + "key", 32 "other": mutexPrefix + "other", 33 } 34 35 for key, expected := range testCases { 36 actual, err := makeLockKey(key) 37 require.NoError(t, err) 38 assert.Equal(t, expected, actual) 39 } 40 }) 41 } 42 43 func lock(t *testing.T, m *Mutex) { 44 t.Helper() 45 46 done := make(chan bool) 47 go func() { 48 t.Helper() 49 50 defer close(done) 51 m.Lock() 52 }() 53 54 select { 55 case <-time.After(1 * time.Second): 56 require.Fail(t, "failed to lock mutex within 1 second") 57 case <-done: 58 } 59 } 60 61 func unlock(t *testing.T, m *Mutex, panics bool) { 62 t.Helper() 63 64 done := make(chan bool) 65 go func() { 66 t.Helper() 67 68 defer close(done) 69 if panics { 70 assert.Panics(t, m.Unlock) 71 } else { 72 assert.NotPanics(t, m.Unlock) 73 } 74 }() 75 76 select { 77 case <-time.After(1 * time.Second): 78 require.Fail(t, "failed to unlock mutex within 1 second") 79 case <-done: 80 } 81 } 82 83 func TestMutex(t *testing.T) { 84 t.Parallel() 85 86 makeKey := model.NewId 87 88 t.Run("successful lock/unlock cycle", func(t *testing.T) { 89 t.Parallel() 90 91 mockPluginAPI := newMockPluginAPI(t) 92 93 m := mustNewMutex(mockPluginAPI, makeKey()) 94 lock(t, m) 95 unlock(t, m, false) 96 lock(t, m) 97 unlock(t, m, false) 98 }) 99 100 t.Run("unlock when not locked", func(t *testing.T) { 101 t.Parallel() 102 103 mockPluginAPI := newMockPluginAPI(t) 104 105 m := mustNewMutex(mockPluginAPI, makeKey()) 106 unlock(t, m, true) 107 }) 108 109 t.Run("blocking lock", func(t *testing.T) { 110 t.Parallel() 111 112 mockPluginAPI := newMockPluginAPI(t) 113 114 m := mustNewMutex(mockPluginAPI, makeKey()) 115 lock(t, m) 116 117 done := make(chan bool) 118 go func() { 119 defer close(done) 120 m.Lock() 121 }() 122 123 select { 124 case <-time.After(1 * time.Second): 125 case <-done: 126 require.Fail(t, "second goroutine should not have locked") 127 } 128 129 unlock(t, m, false) 130 131 select { 132 case <-time.After(pollWaitInterval * 2): 133 require.Fail(t, "second goroutine should have locked") 134 case <-done: 135 } 136 }) 137 138 t.Run("failed lock", func(t *testing.T) { 139 t.Parallel() 140 141 mockPluginAPI := newMockPluginAPI(t) 142 143 m := mustNewMutex(mockPluginAPI, makeKey()) 144 145 mockPluginAPI.setFailing(true) 146 147 done := make(chan bool) 148 go func() { 149 defer close(done) 150 m.Lock() 151 }() 152 153 select { 154 case <-time.After(5 * time.Second): 155 case <-done: 156 require.Fail(t, "goroutine should not have locked") 157 } 158 159 mockPluginAPI.setFailing(false) 160 161 select { 162 case <-time.After(15 * time.Second): 163 require.Fail(t, "goroutine should have locked") 164 case <-done: 165 } 166 }) 167 168 t.Run("failed unlock", func(t *testing.T) { 169 t.Parallel() 170 171 mockPluginAPI := newMockPluginAPI(t) 172 173 key := makeKey() 174 m := mustNewMutex(mockPluginAPI, key) 175 lock(t, m) 176 177 mockPluginAPI.setFailing(true) 178 179 unlock(t, m, false) 180 181 // Simulate expiry 182 mockPluginAPI.clear() 183 mockPluginAPI.setFailing(false) 184 185 lock(t, m) 186 }) 187 188 t.Run("discrete keys", func(t *testing.T) { 189 t.Parallel() 190 191 mockPluginAPI := newMockPluginAPI(t) 192 193 m1 := mustNewMutex(mockPluginAPI, makeKey()) 194 lock(t, m1) 195 196 m2 := mustNewMutex(mockPluginAPI, makeKey()) 197 lock(t, m2) 198 199 m3 := mustNewMutex(mockPluginAPI, makeKey()) 200 lock(t, m3) 201 202 unlock(t, m1, false) 203 unlock(t, m3, false) 204 205 lock(t, m1) 206 207 unlock(t, m2, false) 208 unlock(t, m1, false) 209 }) 210 211 t.Run("with uncancelled context", func(t *testing.T) { 212 t.Parallel() 213 214 mockPluginAPI := newMockPluginAPI(t) 215 216 key := makeKey() 217 m := mustNewMutex(mockPluginAPI, key) 218 219 m.Lock() 220 221 ctx := context.Background() 222 done := make(chan bool) 223 go func() { 224 defer close(done) 225 err := m.LockWithContext(ctx) 226 require.Nil(t, err) 227 }() 228 229 select { 230 case <-time.After(ttl + pollWaitInterval*2): 231 case <-done: 232 require.Fail(t, "goroutine should not have locked") 233 } 234 235 m.Unlock() 236 237 select { 238 case <-time.After(pollWaitInterval * 2): 239 require.Fail(t, "goroutine should have locked after unlock") 240 case <-done: 241 } 242 }) 243 244 t.Run("with canceled context", func(t *testing.T) { 245 t.Parallel() 246 247 mockPluginAPI := newMockPluginAPI(t) 248 249 m := mustNewMutex(mockPluginAPI, makeKey()) 250 251 m.Lock() 252 253 ctx, cancel := context.WithCancel(context.Background()) 254 done := make(chan bool) 255 go func() { 256 defer close(done) 257 err := m.LockWithContext(ctx) 258 require.NotNil(t, err) 259 }() 260 261 select { 262 case <-time.After(ttl + pollWaitInterval*2): 263 case <-done: 264 require.Fail(t, "goroutine should not have locked") 265 } 266 267 cancel() 268 269 select { 270 case <-time.After(pollWaitInterval * 2): 271 require.Fail(t, "goroutine should have aborted after cancellation") 272 case <-done: 273 } 274 }) 275 }