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 }