github.com/voedger/voedger@v0.0.0-20240520144910-273e84102129/pkg/ctrlloop/impl_test.go (about) 1 /* 2 * Copyright (c) 2023-present unTill Pro, Ltd. 3 * @author Alisher Nurmanov 4 */ 5 6 package ctrlloop 7 8 import ( 9 "container/list" 10 "sync" 11 "testing" 12 "time" 13 14 "github.com/stretchr/testify/assert" 15 "github.com/stretchr/testify/require" 16 "github.com/voedger/voedger/pkg/goutils/logger" 17 ) 18 19 func Test_BasicUsage(t *testing.T) { 20 logger.SetLogLevel(logger.LogLevelVerbose) 21 22 mockGetNextTimeFunc := func(cronSchedule string, startTimeTolerance time.Duration, nowTime time.Time) time.Time { 23 return nowTime 24 } 25 26 nextStartTimeFunc = mockGetNextTimeFunc 27 28 tests := []struct { 29 name string 30 numReportedMessages int 31 controller ControllerFunction[string, int, string, int] 32 messages []ControlMessage[string, int] 33 }{ 34 { 35 name: "3 messages:A->B->C", 36 numReportedMessages: 3, 37 controller: func(key string, sp int, state string) (newState *string, pv *int, startTime *time.Time) { 38 logger.Verbose("controllerFunc") 39 v := 1 40 pv = &v 41 return nil, pv, nil 42 }, 43 messages: []ControlMessage[string, int]{ 44 { 45 Key: `A`, 46 SP: 0, 47 CronSchedule: `*/1 * * * *`, 48 StartTimeTolerance: 5 * time.Second, 49 }, 50 { 51 Key: `B`, 52 SP: 1, 53 CronSchedule: `now`, 54 StartTimeTolerance: 5 * time.Second, 55 }, 56 { 57 Key: `C`, 58 SP: 2, 59 CronSchedule: `*/1 * * * *`, 60 StartTimeTolerance: 5 * time.Second, 61 }, 62 }, 63 }, 64 } 65 66 for _, test := range tests { 67 t.Run(test.name, func(t *testing.T) { 68 wg := sync.WaitGroup{} 69 70 mtx := sync.Mutex{} 71 reportDB := make([]struct { 72 Key string 73 PV *int 74 }, 0) 75 76 reporterFunc := func(key string, pv *int) (err error) { 77 mtx.Lock() 78 defer mtx.Unlock() 79 80 logger.Verbose("reporterFunc") 81 82 defer wg.Done() 83 reportDB = append(reportDB, struct { 84 Key string 85 PV *int 86 }{Key: key, PV: pv}) 87 return nil 88 } 89 90 inCh := make(chan ControlMessage[string, int]) 91 92 waitFunc := New(test.controller, reporterFunc, 5, inCh, time.Now) 93 94 wg.Add(test.numReportedMessages) 95 96 for _, m := range test.messages { 97 inCh <- ControlMessage[string, int]{ 98 Key: m.Key, 99 SP: m.SP, 100 CronSchedule: m.CronSchedule, 101 StartTimeTolerance: m.StartTimeTolerance, 102 } 103 } 104 105 wg.Wait() 106 107 close(inCh) 108 109 waitFunc() 110 111 assert.GreaterOrEqual(t, test.numReportedMessages, len(reportDB)) 112 }) 113 } 114 } 115 116 // nolint 117 func Test_SchedulerOnIn(t *testing.T) { 118 alwaysNowFunc := func(cronSchedule string, startTimeTolerance time.Duration, nowTime time.Time) time.Time { 119 return nowTime 120 } 121 122 var testNowTime = time.Date(2023, 4, 20, 00, 00, 00, 0, time.Now().Location()) 123 124 tests := []struct { 125 name string 126 originalMessages []ControlMessage[string, int] 127 scheduledItems []scheduledMessage[string, int, struct{}] 128 nextStartTimeFunc nextStartTimeFunction 129 nowTime time.Time 130 expectedResultKeys []string 131 expectedMaxSerialNumber uint64 132 expectedTopStartTime time.Time 133 }{ 134 { 135 name: `2 keys`, 136 originalMessages: []ControlMessage[string, int]{ 137 { 138 Key: `A`, 139 SP: 0, 140 }, 141 { 142 Key: `B`, 143 SP: 4, 144 }, 145 }, 146 scheduledItems: []scheduledMessage[string, int, struct{}]{}, 147 nextStartTimeFunc: alwaysNowFunc, 148 nowTime: testNowTime, 149 expectedResultKeys: []string{`A`, `B`}, 150 expectedMaxSerialNumber: 1, 151 expectedTopStartTime: testNowTime, 152 }, 153 { 154 name: `2 keys, 1 key in ScheduledItems`, 155 originalMessages: []ControlMessage[string, int]{ 156 { 157 Key: `A`, 158 SP: 0, 159 }, 160 { 161 Key: `B`, 162 SP: 4, 163 }, 164 }, 165 scheduledItems: []scheduledMessage[string, int, struct{}]{ 166 { 167 Key: `A`, 168 SP: 0, 169 serialNumber: 10, 170 StartTime: testNowTime, 171 }, 172 }, 173 nextStartTimeFunc: alwaysNowFunc, 174 nowTime: testNowTime, 175 expectedResultKeys: []string{`A`, `B`}, 176 expectedMaxSerialNumber: 10, 177 expectedTopStartTime: testNowTime, 178 }, 179 { 180 name: `invalid CronSchedule`, 181 originalMessages: []ControlMessage[string, int]{ 182 { 183 Key: `A`, 184 SP: 0, 185 CronSchedule: `QWERTY`, 186 }, 187 }, 188 nowTime: testNowTime, 189 scheduledItems: []scheduledMessage[string, int, struct{}]{}, 190 nextStartTimeFunc: getNextStartTime, 191 expectedResultKeys: []string{`A`}, 192 expectedMaxSerialNumber: 0, 193 expectedTopStartTime: testNowTime, 194 }, 195 { 196 name: `CronSchedule * 1 * * *, tolerance zero`, 197 originalMessages: []ControlMessage[string, int]{ 198 { 199 Key: `A`, 200 SP: 0, 201 CronSchedule: `* 1 * * *`, 202 }, 203 }, 204 scheduledItems: []scheduledMessage[string, int, struct{}]{}, 205 nextStartTimeFunc: getNextStartTime, 206 nowTime: testNowTime, 207 expectedResultKeys: []string{`A`}, 208 expectedMaxSerialNumber: 0, 209 expectedTopStartTime: testNowTime.Add(1 * time.Hour), 210 }, 211 { 212 name: `CronSchedule 0 0 * * *, tolerance 5 min`, 213 originalMessages: []ControlMessage[string, int]{ 214 { 215 Key: `A`, 216 SP: 0, 217 CronSchedule: `0 0 * * *`, 218 StartTimeTolerance: 5 * time.Minute, 219 }, 220 }, 221 scheduledItems: []scheduledMessage[string, int, struct{}]{}, 222 nextStartTimeFunc: getNextStartTime, 223 nowTime: testNowTime.Add(299 * time.Second), 224 expectedResultKeys: []string{`A`}, 225 expectedMaxSerialNumber: 0, 226 expectedTopStartTime: testNowTime, 227 }, 228 { 229 name: `CronSchedule 0 0 * * *, tolerance 5 min, 1 second delay`, 230 originalMessages: []ControlMessage[string, int]{ 231 { 232 Key: `A`, 233 SP: 0, 234 CronSchedule: `0 0 * * *`, 235 StartTimeTolerance: 5 * time.Minute, 236 }, 237 }, 238 scheduledItems: []scheduledMessage[string, int, struct{}]{}, 239 nextStartTimeFunc: getNextStartTime, 240 nowTime: testNowTime.Add(301 * time.Second), 241 expectedResultKeys: []string{`A`}, 242 expectedMaxSerialNumber: 0, 243 expectedTopStartTime: testNowTime.Add(24 * time.Hour), 244 }, 245 { 246 name: `the second message scheduled before the first one`, 247 originalMessages: []ControlMessage[string, int]{ 248 { 249 Key: `A`, 250 SP: 0, 251 CronSchedule: `0 11 * * *`, 252 }, 253 { 254 Key: `B`, 255 SP: 4, 256 CronSchedule: `0 10 * * *`, 257 }, 258 }, 259 scheduledItems: []scheduledMessage[string, int, struct{}]{}, 260 nextStartTimeFunc: getNextStartTime, 261 nowTime: testNowTime, 262 expectedResultKeys: []string{`B`, `A`}, 263 expectedMaxSerialNumber: 1, 264 expectedTopStartTime: testNowTime.Add(10 * time.Hour), 265 }, 266 } 267 268 for _, test := range tests { 269 t.Run(test.name, func(t *testing.T) { 270 nextStartTimeFunc = test.nextStartTimeFunc 271 272 initList := list.New() 273 for _, i := range test.scheduledItems { 274 initList.PushBack(i) 275 } 276 277 schedulerObj := newScheduler[string, int, struct{}](initList) 278 279 for i, m := range test.originalMessages { 280 schedulerObj.OnIn(uint64(i), m, test.nowTime) 281 } 282 283 maxSerialNumber := uint64(0) 284 resultKeys := make([]string, 0) 285 for element := schedulerObj.scheduledItems.Front(); element != nil; element = element.Next() { 286 m := element.Value.(scheduledMessage[string, int, struct{}]) 287 if m.serialNumber > maxSerialNumber { 288 maxSerialNumber = m.serialNumber 289 } 290 resultKeys = append(resultKeys, m.Key) 291 } 292 293 top := schedulerObj.scheduledItems.Front().Value.(scheduledMessage[string, int, struct{}]) 294 require.Equal(t, test.expectedTopStartTime, top.StartTime) 295 require.Equal(t, test.expectedResultKeys, resultKeys) 296 require.Equal(t, test.expectedMaxSerialNumber, maxSerialNumber) 297 }) 298 } 299 } 300 301 func Test_SchedulerOnRepeat(t *testing.T) { 302 var testNowTime = time.Date(2023, 4, 20, 00, 00, 00, 0, time.Now().Location()) 303 304 tests := []struct { 305 name string 306 messagesToRepeat []scheduledMessage[string, int, struct{}] 307 scheduledItems []scheduledMessage[string, int, struct{}] 308 expectedScheduledKeys []string 309 expectedMaxSerialNumber uint64 310 }{ 311 { 312 name: `fresh serial number`, 313 messagesToRepeat: []scheduledMessage[string, int, struct{}]{ 314 { 315 Key: `A`, 316 SP: 0, 317 serialNumber: 1, 318 StartTime: testNowTime, 319 }, 320 { 321 Key: `A`, 322 SP: 1, 323 serialNumber: 2, 324 StartTime: testNowTime.Add(5 * time.Second), 325 }, 326 }, 327 expectedScheduledKeys: []string{`A`}, 328 expectedMaxSerialNumber: 2, 329 }, 330 { 331 name: `obsoleted serial number`, 332 messagesToRepeat: []scheduledMessage[string, int, struct{}]{ 333 { 334 Key: `A`, 335 SP: 0, 336 serialNumber: 2, 337 StartTime: testNowTime, 338 }, 339 { 340 Key: `A`, 341 SP: 1, 342 serialNumber: 1, 343 StartTime: testNowTime.Add(5 * time.Second), 344 }, 345 }, 346 expectedScheduledKeys: []string{`A`}, 347 expectedMaxSerialNumber: 2, 348 }, 349 } 350 351 for _, test := range tests { 352 t.Run(test.name, func(t *testing.T) { 353 initList := list.New() 354 schedulerObj := newScheduler[string, int, struct{}](initList) 355 356 for _, m := range test.messagesToRepeat { 357 schedulerObj.OnRepeat(m, time.Now()) 358 } 359 360 maxSerialNumber := uint64(0) 361 resultKeys := make([]string, 0) 362 for element := schedulerObj.scheduledItems.Front(); element != nil; element = element.Next() { 363 m := element.Value.(scheduledMessage[string, int, struct{}]) 364 if m.serialNumber > maxSerialNumber { 365 maxSerialNumber = m.serialNumber 366 } 367 resultKeys = append(resultKeys, m.Key) 368 } 369 370 require.Equal(t, test.expectedScheduledKeys, resultKeys) 371 require.Equal(t, test.expectedMaxSerialNumber, maxSerialNumber) 372 }) 373 } 374 } 375 376 func Test_SchedulerOnTimer(t *testing.T) { 377 var testNowTime = time.Date(2023, 4, 20, 00, 00, 00, 0, time.Now().Location()) 378 379 t.Run(`empty scheduledItems`, func(t *testing.T) { 380 dedupInCh := make(chan statefulMessage[string, int, struct{}], 10) 381 382 schedulerObj := newScheduler[string, int, struct{}](nil) 383 schedulerObj.OnTimer(dedupInCh, time.Now()) 384 schedulerObj.OnTimer(dedupInCh, time.Now()) 385 386 messagesToDedupIn := testMessagesReader(dedupInCh) 387 388 // closing channels 389 close(dedupInCh) 390 391 require.Empty(t, messagesToDedupIn) 392 }) 393 394 t.Run(`2 scheduled items`, func(t *testing.T) { 395 dedupInCh := make(chan statefulMessage[string, int, struct{}], 10) 396 397 scheduledItems := []scheduledMessage[string, int, struct{}]{ 398 { 399 Key: `A`, 400 SP: 0, 401 serialNumber: 1, 402 StartTime: testNowTime.Add(5 * time.Second), 403 }, 404 { 405 Key: `B`, 406 SP: 1, 407 serialNumber: 2, 408 StartTime: testNowTime.Add(3 * time.Second), 409 }, 410 } 411 412 schedulerObj := newScheduler[string, int, struct{}](nil) 413 // fulfilling ScheduledItems storage 414 for _, m := range scheduledItems { 415 schedulerObj.OnRepeat(m, testNowTime) 416 } 417 require.Equal(t, 3*time.Second, schedulerObj.lastDuration) 418 419 schedulerObj.OnTimer(dedupInCh, testNowTime) 420 require.Equal(t, 5*time.Second, schedulerObj.lastDuration) 421 422 // closing channels 423 close(dedupInCh) 424 }) 425 426 t.Run(`dedupIn channel is busy`, func(t *testing.T) { 427 dedupInCh := make(chan statefulMessage[string, int, struct{}], 1) 428 429 scheduledItems := []scheduledMessage[string, int, struct{}]{ 430 { 431 Key: `A`, 432 SP: 0, 433 serialNumber: 1, 434 StartTime: testNowTime.Add(5 * time.Second), 435 }, 436 } 437 438 schedulerObj := newScheduler[string, int, struct{}](nil) 439 // fulfilling ScheduledItems storage 440 for _, m := range scheduledItems { 441 schedulerObj.OnRepeat(m, testNowTime) 442 } 443 require.Equal(t, 5*time.Second, schedulerObj.lastDuration) 444 445 // make dedupInCh busy 446 dedupInCh <- statefulMessage[string, int, struct{}]{ 447 Key: `B`, 448 SP: 1, 449 serialNumber: 2, 450 } 451 schedulerObj.OnTimer(dedupInCh, testNowTime) 452 require.Equal(t, DedupInRetryInterval, schedulerObj.lastDuration) 453 454 // closing channels 455 close(dedupInCh) 456 }) 457 } 458 459 func Test_Dedupin(t *testing.T) { 460 mockGetNextTimeFunc := func(cronSchedule string, startTimeTolerance time.Duration, nowTime time.Time) time.Time { 461 return nowTime 462 } 463 464 nextStartTimeFunc = mockGetNextTimeFunc 465 466 tests := []struct { 467 name string 468 inProcessKeys []string 469 messages []statefulMessage[string, int, struct{}] 470 }{ 471 { 472 name: `2 keys are duplicated in 4 messages`, 473 inProcessKeys: []string{``}, 474 messages: []statefulMessage[string, int, struct{}]{ 475 { 476 Key: `A`, 477 SP: 0, 478 serialNumber: 1, 479 }, 480 { 481 Key: `B`, 482 SP: 1, 483 serialNumber: 1, 484 }, 485 { 486 Key: `B`, 487 SP: 2, 488 serialNumber: 1, 489 }, 490 { 491 Key: `C`, 492 SP: 2, 493 serialNumber: 1, 494 }, 495 }, 496 }, 497 } 498 499 for _, test := range tests { 500 t.Run(test.name, func(t *testing.T) { 501 InProcess := sync.Map{} 502 dedupInCh := make(chan statefulMessage[string, int, struct{}]) 503 callerCh := make(chan statefulMessage[string, int, struct{}], 10) 504 repeatCh := make(chan scheduledMessage[string, int, struct{}], 10) 505 506 var messagesToCall []statefulMessage[string, int, struct{}] 507 var messagesToRepeat []scheduledMessage[string, int, struct{}] 508 var inProcessKeyCounter int 509 go func() { 510 testMessagesWriter(dedupInCh, test.messages) 511 512 // closing channels 513 close(dedupInCh) 514 close(repeatCh) 515 }() 516 517 dedupIn(dedupInCh, callerCh, repeatCh, &InProcess, time.Now) 518 519 messagesToCall = testMessagesReader(callerCh) 520 messagesToRepeat = testMessagesReader(repeatCh) 521 522 inProcessKeyCounter = 0 523 InProcess.Range(func(_, _ any) bool { 524 inProcessKeyCounter++ 525 return true 526 }) 527 528 require.Len(t, messagesToCall, inProcessKeyCounter) 529 require.Len(t, messagesToRepeat, 1) 530 }) 531 } 532 } 533 534 func Test_Repeater(t *testing.T) { 535 mockGetNextTimeFunc := func(cronSchedule string, startTimeTolerance time.Duration, nowTime time.Time) time.Time { 536 return nowTime 537 } 538 539 nextStartTimeFunc = mockGetNextTimeFunc 540 541 now := time.Now() 542 pv := 1 543 544 tests := []struct { 545 name string 546 inProcessKeys []string 547 messages []answer[string, int, int, struct{}] 548 }{ 549 { 550 name: `2 messages to report, 2 messages to repeat`, 551 inProcessKeys: []string{``}, 552 messages: []answer[string, int, int, struct{}]{ 553 { 554 Key: `A`, 555 SP: 0, 556 serialNumber: 1, 557 StartTime: &now, 558 PV: nil, 559 }, 560 { 561 Key: `B`, 562 SP: 1, 563 serialNumber: 1, 564 StartTime: &now, 565 PV: nil, 566 }, 567 { 568 Key: `C`, 569 SP: 2, 570 serialNumber: 1, 571 StartTime: nil, 572 PV: &pv, 573 }, 574 { 575 Key: `D`, 576 SP: 2, 577 serialNumber: 1, 578 StartTime: nil, 579 PV: &pv, 580 }, 581 }, 582 }, 583 } 584 585 for _, test := range tests { 586 t.Run(test.name, func(t *testing.T) { 587 repeaterCh := make(chan answer[string, int, int, struct{}]) 588 repeatCh := make(chan scheduledMessage[string, int, struct{}], 10) 589 reporterCh := make(chan reportInfo[string, int], 10) 590 591 var messagesToReport []reportInfo[string, int] 592 var messagesToRepeat []scheduledMessage[string, int, struct{}] 593 go func() { 594 testMessagesWriter(repeaterCh, test.messages) 595 596 close(repeaterCh) 597 }() 598 599 repeater(repeaterCh, repeatCh, reporterCh) 600 601 messagesToReport = testMessagesReader(reporterCh) 602 messagesToRepeat = testMessagesReader(repeatCh) 603 604 require.Len(t, messagesToReport, 2) 605 require.Len(t, messagesToRepeat, 2) 606 }) 607 } 608 } 609 610 func testMessagesWriter[T any](ch chan<- T, arr []T) { 611 for _, m := range arr { 612 ch <- m 613 } 614 } 615 616 func testMessagesReader[T any](ch <-chan T) []T { 617 results := make([]T, 0) 618 619 var val T 620 ok := true 621 for ok { 622 select { 623 case val, ok = <-ch: 624 if ok { 625 results = append(results, val) 626 } else { 627 return results 628 } 629 default: 630 return results 631 } 632 } 633 return results 634 }