github.com/mattermost/mattermost-plugin-api@v0.1.4/cluster/job_test.go (about)

     1  package cluster
     2  
     3  import (
     4  	"sync"
     5  	"sync/atomic"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/mattermost/mattermost-server/v6/model"
    10  	"github.com/stretchr/testify/assert"
    11  	"github.com/stretchr/testify/require"
    12  )
    13  
    14  func TestMakeWaitForInterval(t *testing.T) {
    15  	t.Run("panics on invalid interval", func(t *testing.T) {
    16  		assert.Panics(t, func() {
    17  			MakeWaitForInterval(0)
    18  		})
    19  	})
    20  
    21  	const neverRun = -1 * time.Second
    22  
    23  	testCases := []struct {
    24  		Description  string
    25  		Interval     time.Duration
    26  		LastFinished time.Duration
    27  		Expected     time.Duration
    28  	}{
    29  		{
    30  			"never run, 5 minutes",
    31  			5 * time.Minute,
    32  			neverRun,
    33  			0,
    34  		},
    35  		{
    36  			"run 1 minute ago, 5 minutes",
    37  			5 * time.Minute,
    38  			-1 * time.Minute,
    39  			4 * time.Minute,
    40  		},
    41  		{
    42  			"run 2 minutes ago, 5 minutes",
    43  			5 * time.Minute,
    44  			-2 * time.Minute,
    45  			3 * time.Minute,
    46  		},
    47  		{
    48  			"run 4 minutes 30 seconds ago, 5 minutes",
    49  			5 * time.Minute,
    50  			-4*time.Minute - 30*time.Second,
    51  			30 * time.Second,
    52  		},
    53  		{
    54  			"run 4 minutes 59 seconds ago, 5 minutes",
    55  			5 * time.Minute,
    56  			-4*time.Minute - 59*time.Second,
    57  			1 * time.Second,
    58  		},
    59  		{
    60  			"never run, 1 hour",
    61  			1 * time.Hour,
    62  			neverRun,
    63  			0,
    64  		},
    65  		{
    66  			"run 1 minute ago, 1 hour",
    67  			1 * time.Hour,
    68  			-1 * time.Minute,
    69  			59 * time.Minute,
    70  		},
    71  		{
    72  			"run 20 minutes ago, 1 hour",
    73  			1 * time.Hour,
    74  			-20 * time.Minute,
    75  			40 * time.Minute,
    76  		},
    77  		{
    78  			"run 55 minutes 30 seconds ago, 1 hour",
    79  			1 * time.Hour,
    80  			-55*time.Minute - 30*time.Second,
    81  			4*time.Minute + 30*time.Second,
    82  		},
    83  		{
    84  			"run 59 minutes 59 seconds ago, 1 hour",
    85  			1 * time.Hour,
    86  			-59*time.Minute - 59*time.Second,
    87  			1 * time.Second,
    88  		},
    89  	}
    90  
    91  	for _, testCase := range testCases {
    92  		t.Run(testCase.Description, func(t *testing.T) {
    93  			now := time.Now()
    94  
    95  			var lastFinished time.Time
    96  			if testCase.LastFinished != neverRun {
    97  				lastFinished = now.Add(testCase.LastFinished)
    98  			}
    99  
   100  			actual := MakeWaitForInterval(testCase.Interval)(now, JobMetadata{
   101  				LastFinished: lastFinished,
   102  			})
   103  			assert.Equal(t, testCase.Expected, actual)
   104  		})
   105  	}
   106  }
   107  
   108  func TestMakeWaitForRoundedInterval(t *testing.T) {
   109  	t.Run("panics on invalid interval", func(t *testing.T) {
   110  		assert.Panics(t, func() {
   111  			MakeWaitForRoundedInterval(0)
   112  		})
   113  	})
   114  
   115  	const neverRun = -1 * time.Second
   116  	topOfTheHour := time.Now().Truncate(1 * time.Hour)
   117  	topOfTheDay := time.Now().Truncate(24 * time.Hour)
   118  
   119  	testCases := []struct {
   120  		Description  string
   121  		Interval     time.Duration
   122  		Now          time.Time
   123  		LastFinished time.Duration
   124  		Expected     time.Duration
   125  	}{
   126  		{
   127  			"5 minutes, top of the hour, never run",
   128  			5 * time.Minute,
   129  			topOfTheHour,
   130  			neverRun,
   131  			0,
   132  		},
   133  		{
   134  			"5 minutes, top of the hour less 1 minute, never run",
   135  			5 * time.Minute,
   136  			topOfTheHour.Add(-1 * time.Minute),
   137  			neverRun,
   138  			0,
   139  		},
   140  		{
   141  			"5 minutes, top of the hour less 1 minute, run 1 minute ago",
   142  			5 * time.Minute,
   143  			topOfTheHour.Add(-1 * time.Minute),
   144  			-1 * time.Minute,
   145  			1 * time.Minute,
   146  		},
   147  		{
   148  			"5 minutes, top of the hour plus 1 minute, run 2 minutes ago",
   149  			5 * time.Minute,
   150  			topOfTheHour.Add(1 * time.Minute),
   151  			-2 * time.Minute,
   152  			0,
   153  		},
   154  		{
   155  			"5 minutes, top of the hour plus 1 minute, run 30 seconds ago",
   156  			5 * time.Minute,
   157  			topOfTheHour.Add(1 * time.Minute),
   158  			-30 * time.Second,
   159  			4 * time.Minute,
   160  		},
   161  		{
   162  			"5 minutes, top of the hour plus 7 minutes, run 30 seconds ago",
   163  			5 * time.Minute,
   164  			topOfTheHour.Add(7 * time.Minute),
   165  			-30 * time.Second,
   166  			3 * time.Minute,
   167  		},
   168  		{
   169  			"30 minutes, top of the hour, never run",
   170  			30 * time.Minute,
   171  			topOfTheHour,
   172  			neverRun,
   173  			0,
   174  		},
   175  		{
   176  			"30 minutes, top of the hour less 1 minute, never run",
   177  			30 * time.Minute,
   178  			topOfTheHour.Add(-1 * time.Minute),
   179  			neverRun,
   180  			0,
   181  		},
   182  		{
   183  			"30 minutes, top of the hour less 1 minute, run 1 minute ago",
   184  			30 * time.Minute,
   185  			topOfTheHour.Add(-1 * time.Minute),
   186  			-1 * time.Minute,
   187  			1 * time.Minute,
   188  		},
   189  		{
   190  			"30 minutes, top of the hour plus 1 minute, run 2 minutes ago",
   191  			30 * time.Minute,
   192  			topOfTheHour.Add(1 * time.Minute),
   193  			-2 * time.Minute,
   194  			0,
   195  		},
   196  		{
   197  			"30 minutes, top of the hour plus 1 minute, run 30 seconds ago",
   198  			30 * time.Minute,
   199  			topOfTheHour.Add(1 * time.Minute),
   200  			-30 * time.Second,
   201  			29 * time.Minute,
   202  		},
   203  		{
   204  			"30 minutes, top of the hour plus 7 minutes, run 30 seconds ago",
   205  			30 * time.Minute,
   206  			topOfTheHour.Add(7 * time.Minute),
   207  			-30 * time.Second,
   208  			23 * time.Minute,
   209  		},
   210  		{
   211  			"24 hours, top of the day, never run",
   212  			24 * time.Hour,
   213  			topOfTheDay,
   214  			neverRun,
   215  			0,
   216  		},
   217  		{
   218  			"24 hours, top of the day less 1 minute, never run",
   219  			24 * time.Hour,
   220  			topOfTheDay.Add(-1 * time.Minute),
   221  			neverRun,
   222  			0,
   223  		},
   224  		{
   225  			"24 hours, top of the day less 1 minute, run 1 minute ago",
   226  			24 * time.Hour,
   227  			topOfTheDay.Add(-1 * time.Minute),
   228  			-1 * time.Minute,
   229  			1 * time.Minute,
   230  		},
   231  		{
   232  			"24 hours, top of the day plus 1 minute, run 2 minutes ago",
   233  			24 * time.Hour,
   234  			topOfTheDay.Add(1 * time.Minute),
   235  			-2 * time.Minute,
   236  			0,
   237  		},
   238  		{
   239  			"24 hours, top of the day plus 1 minute, run 30 seconds ago",
   240  			24 * time.Hour,
   241  			topOfTheDay.Add(1 * time.Minute),
   242  			-30 * time.Second,
   243  			23*time.Hour + 59*time.Minute,
   244  		},
   245  		{
   246  			"24 hours, top of the day plus 7 minutes, run 30 seconds ago",
   247  			24 * time.Hour,
   248  			topOfTheDay.Add(7 * time.Minute),
   249  			-30 * time.Second,
   250  			23*time.Hour + 53*time.Minute,
   251  		},
   252  	}
   253  
   254  	for _, testCase := range testCases {
   255  		t.Run(testCase.Description, func(t *testing.T) {
   256  			var lastFinished time.Time
   257  			if testCase.LastFinished != neverRun {
   258  				lastFinished = testCase.Now.Add(testCase.LastFinished)
   259  			}
   260  
   261  			actual := MakeWaitForRoundedInterval(testCase.Interval)(testCase.Now, JobMetadata{
   262  				LastFinished: lastFinished,
   263  			})
   264  			assert.Equal(t, testCase.Expected, actual)
   265  		})
   266  	}
   267  }
   268  
   269  func TestSchedule(t *testing.T) {
   270  	t.Parallel()
   271  
   272  	makeKey := model.NewId
   273  
   274  	t.Run("single-threaded", func(t *testing.T) {
   275  		t.Parallel()
   276  
   277  		mockPluginAPI := newMockPluginAPI(t)
   278  
   279  		count := new(int32)
   280  		callback := func() {
   281  			atomic.AddInt32(count, 1)
   282  		}
   283  
   284  		job, err := Schedule(mockPluginAPI, makeKey(), MakeWaitForInterval(100*time.Millisecond), callback)
   285  		require.NoError(t, err)
   286  		require.NotNil(t, job)
   287  
   288  		time.Sleep(1 * time.Second)
   289  
   290  		err = job.Close()
   291  		require.NoError(t, err)
   292  
   293  		time.Sleep(1 * time.Second)
   294  
   295  		// Shouldn't have hit 20 in this time frame
   296  		assert.Less(t, *count, int32(20))
   297  
   298  		// Should have hit at least 5 in this time frame
   299  		assert.Greater(t, *count, int32(5))
   300  	})
   301  
   302  	t.Run("multi-threaded, single job", func(t *testing.T) {
   303  		t.Parallel()
   304  
   305  		mockPluginAPI := newMockPluginAPI(t)
   306  
   307  		count := new(int32)
   308  		callback := func() {
   309  			atomic.AddInt32(count, 1)
   310  		}
   311  
   312  		var jobs []*Job
   313  
   314  		key := makeKey()
   315  
   316  		for i := 0; i < 3; i++ {
   317  			job, err := Schedule(mockPluginAPI, key, MakeWaitForInterval(100*time.Millisecond), callback)
   318  			require.NoError(t, err)
   319  			require.NotNil(t, job)
   320  
   321  			jobs = append(jobs, job)
   322  		}
   323  
   324  		time.Sleep(1 * time.Second)
   325  
   326  		var wg sync.WaitGroup
   327  		for i := 0; i < 3; i++ {
   328  			job := jobs[i]
   329  			wg.Add(1)
   330  			go func() {
   331  				defer wg.Done()
   332  				err := job.Close()
   333  				require.NoError(t, err)
   334  			}()
   335  		}
   336  		wg.Wait()
   337  
   338  		time.Sleep(1 * time.Second)
   339  
   340  		// Shouldn't have hit 20 in this time frame
   341  		assert.Less(t, *count, int32(20))
   342  
   343  		// Should have hit at least 5 in this time frame
   344  		assert.Greater(t, *count, int32(5))
   345  	})
   346  
   347  	t.Run("multi-threaded, multiple jobs", func(t *testing.T) {
   348  		t.Parallel()
   349  
   350  		mockPluginAPI := newMockPluginAPI(t)
   351  
   352  		countA := new(int32)
   353  		callbackA := func() {
   354  			atomic.AddInt32(countA, 1)
   355  		}
   356  
   357  		countB := new(int32)
   358  		callbackB := func() {
   359  			atomic.AddInt32(countB, 1)
   360  		}
   361  
   362  		keyA := makeKey()
   363  		keyB := makeKey()
   364  
   365  		var jobs []*Job
   366  		for i := 0; i < 3; i++ {
   367  			var key string
   368  			var callback func()
   369  			if i <= 1 {
   370  				key = keyA
   371  				callback = callbackA
   372  			} else {
   373  				key = keyB
   374  				callback = callbackB
   375  			}
   376  
   377  			job, err := Schedule(mockPluginAPI, key, MakeWaitForInterval(100*time.Millisecond), callback)
   378  			require.NoError(t, err)
   379  			require.NotNil(t, job)
   380  
   381  			jobs = append(jobs, job)
   382  		}
   383  
   384  		time.Sleep(1 * time.Second)
   385  
   386  		var wg sync.WaitGroup
   387  		for i := 0; i < 3; i++ {
   388  			job := jobs[i]
   389  			wg.Add(1)
   390  			go func() {
   391  				defer wg.Done()
   392  				err := job.Close()
   393  				require.NoError(t, err)
   394  			}()
   395  		}
   396  		wg.Wait()
   397  
   398  		time.Sleep(1 * time.Second)
   399  
   400  		// Shouldn't have hit 20 in this time frame
   401  		assert.Less(t, *countA, int32(20))
   402  
   403  		// Should have hit at least 5 in this time frame
   404  		assert.Greater(t, *countA, int32(5))
   405  
   406  		// Shouldn't have hit 20 in this time frame
   407  		assert.Less(t, *countB, int32(20))
   408  
   409  		// Should have hit at least 5 in this time frame
   410  		assert.Greater(t, *countB, int32(5))
   411  	})
   412  }