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  }