@@ -559,3 +559,83 @@ func TestDuplicateMessageIDFromStreamWhenAlreadyInHeap_IsSkippedByHeapAndHandleM
559559 time .Sleep (2 * time .Second )
560560 require .True (t , mock .AssertExpectationsForObjects (t , mockExecutor ))
561561}
562+
563+ // TestGracefulShutdown tests that when Close() is called while a message is being processed, the processing loop will
564+ // shut down gracefully. This state is simulated by blocking the HandleMessage() call until Close() is called, and then
565+ // asserting that we logged the message about dropping a payload to exit.
566+ func TestGracefulShutdown (t * testing.T ) {
567+ lggr , hook := logger .TestObserved (t , zapcore .InfoLevel )
568+ currentTime := time .Now ().UTC ()
569+ mockTimeProvider := mocks .NewMockTimeProvider (t )
570+ mockTimeProvider .EXPECT ().GetTime ().Return (currentTime ).Maybe ()
571+
572+ seqNum := uint64 (0 )
573+ messageGenerator := func () common.MessageWithMetadata {
574+ seqNum ++
575+ return common.MessageWithMetadata {
576+ Message : protocol.Message {
577+ DestChainSelector : 1 ,
578+ SourceChainSelector : 2 ,
579+ SequenceNumber : protocol .SequenceNumber (seqNum ),
580+ },
581+ Metadata : common.MessageMetadata {
582+ IngestionTimestamp : currentTime ,
583+ },
584+ }
585+ }
586+
587+ results := make (chan common.MessageWithMetadata , 1 )
588+ results <- messageGenerator ()
589+ messageSubscriber := mocks .NewMockMessageSubscriber (t )
590+ messageSubscriber .EXPECT ().Start (mock .Anything ).Return (results , nil , nil )
591+
592+ unblockHandle := make (chan struct {})
593+ mockExecutor := mocks .NewMockExecutor (t )
594+ mockExecutor .EXPECT ().Start (mock .Anything ).Return (nil )
595+ mockExecutor .EXPECT ().CheckValidMessage (mock .Anything , mock .Anything ).Return (nil ).Maybe ()
596+ mockExecutor .EXPECT ().HandleMessage (mock .Anything , mock .Anything ).Run (func (context.Context , protocol.Message ) {
597+ <- unblockHandle
598+ }).Return (false , nil ).Maybe ()
599+
600+ leaderElector := mocks .NewMockLeaderElector (t )
601+ leaderElector .EXPECT ().GetReadyTimestamp (mock .Anything , mock .Anything , mock .Anything ).Return (currentTime ).Maybe ()
602+ leaderElector .EXPECT ().GetRetryDelay (mock .Anything ).Return (time .Second ).Maybe ()
603+
604+ ec , err := executor .NewCoordinator (
605+ lggr ,
606+ mockExecutor ,
607+ messageSubscriber ,
608+ leaderElector ,
609+ monitoring .NewNoopExecutorMonitoring (),
610+ 8 * time .Hour ,
611+ mockTimeProvider ,
612+ 1 ,
613+ )
614+ require .NoError (t , err )
615+ require .NotNil (t , ec )
616+
617+ ctx , cancel := context .WithTimeout (context .Background (), 10 * time .Second )
618+ defer cancel ()
619+
620+ require .NoError (t , ec .Start (ctx ))
621+ time .Sleep (1 * time .Second )
622+
623+ // Block close for 1 second to ensure the processing loop is forced to drop a payload.
624+ go func () {
625+ time .Sleep (1 * time .Second )
626+ close (unblockHandle )
627+ }()
628+ require .NoError (t , ec .Close ())
629+
630+ // Assert that we logged the message about dropping a payload.
631+ found := func () bool {
632+ for _ , entry := range hook .All () {
633+ entryStr := fmt .Sprintf ("%+v" , entry )
634+ if strings .Contains (entryStr , "Processing loop dropping payload to exit" ) {
635+ return true
636+ }
637+ }
638+ return false
639+ }
640+ require .Eventuallyf (t , found , 2 * time .Second , 100 * time .Millisecond , "executor coordinator did not stop in time" )
641+ }
0 commit comments