1
+ import type { TaskRunnersConfig } from '@n8n/config' ;
1
2
import type { RunnerMessage , TaskResultData } from '@n8n/task-runner' ;
2
3
import { mock } from 'jest-mock-extended' ;
3
- import type { INodeTypeBaseDescription } from 'n8n-workflow' ;
4
+ import { ApplicationError , type INodeTypeBaseDescription } from 'n8n-workflow' ;
5
+
6
+ import { Time } from '@/constants' ;
4
7
5
8
import { TaskRejectError } from '../errors' ;
9
+ import type { RunnerLifecycleEvents } from '../runner-lifecycle-events' ;
6
10
import { TaskBroker } from '../task-broker.service' ;
7
11
import type { TaskOffer , TaskRequest , TaskRunner } from '../task-broker.service' ;
8
12
@@ -12,7 +16,7 @@ describe('TaskBroker', () => {
12
16
let taskBroker : TaskBroker ;
13
17
14
18
beforeEach ( ( ) => {
15
- taskBroker = new TaskBroker ( mock ( ) ) ;
19
+ taskBroker = new TaskBroker ( mock ( ) , mock ( ) , mock ( ) ) ;
16
20
jest . restoreAllMocks ( ) ;
17
21
} ) ;
18
22
@@ -618,4 +622,131 @@ describe('TaskBroker', () => {
618
622
} ) ;
619
623
} ) ;
620
624
} ) ;
625
+
626
+ describe ( 'task timeouts' , ( ) => {
627
+ let taskBroker : TaskBroker ;
628
+ let config : TaskRunnersConfig ;
629
+ let runnerLifecycleEvents = mock < RunnerLifecycleEvents > ( ) ;
630
+
631
+ beforeAll ( ( ) => {
632
+ jest . useFakeTimers ( ) ;
633
+ config = mock < TaskRunnersConfig > ( { taskTimeout : 30 } ) ;
634
+ taskBroker = new TaskBroker ( mock ( ) , config , runnerLifecycleEvents ) ;
635
+ } ) ;
636
+
637
+ afterAll ( ( ) => {
638
+ jest . useRealTimers ( ) ;
639
+ } ) ;
640
+
641
+ it ( 'on sending task, we should set up task timeout' , async ( ) => {
642
+ jest . spyOn ( global , 'setTimeout' ) ;
643
+
644
+ const taskId = 'task1' ;
645
+ const runnerId = 'runner1' ;
646
+ const runner = mock < TaskRunner > ( { id : runnerId } ) ;
647
+ const runnerMessageCallback = jest . fn ( ) ;
648
+
649
+ taskBroker . registerRunner ( runner , runnerMessageCallback ) ;
650
+ taskBroker . setTasks ( {
651
+ [ taskId ] : { id : taskId , runnerId, requesterId : 'requester1' , taskType : 'test' } ,
652
+ } ) ;
653
+
654
+ await taskBroker . sendTaskSettings ( taskId , { } ) ;
655
+
656
+ expect ( setTimeout ) . toHaveBeenCalledWith (
657
+ expect . any ( Function ) ,
658
+ config . taskTimeout * Time . seconds . toMilliseconds ,
659
+ ) ;
660
+ } ) ;
661
+
662
+ it ( 'on task completion, we should clear timeout' , async ( ) => {
663
+ jest . spyOn ( global , 'clearTimeout' ) ;
664
+
665
+ const taskId = 'task1' ;
666
+ const runnerId = 'runner1' ;
667
+ const requesterId = 'requester1' ;
668
+ const requesterCallback = jest . fn ( ) ;
669
+
670
+ taskBroker . registerRequester ( requesterId , requesterCallback ) ;
671
+ taskBroker . setTasks ( {
672
+ [ taskId ] : {
673
+ id : taskId ,
674
+ runnerId,
675
+ requesterId,
676
+ taskType : 'test' ,
677
+ timeout : setTimeout ( ( ) => { } , config . taskTimeout * Time . seconds . toMilliseconds ) ,
678
+ } ,
679
+ } ) ;
680
+
681
+ await taskBroker . taskDoneHandler ( taskId , { result : [ ] } ) ;
682
+
683
+ expect ( clearTimeout ) . toHaveBeenCalled ( ) ;
684
+ expect ( taskBroker . getTasks ( ) . get ( taskId ) ) . toBeUndefined ( ) ;
685
+ } ) ;
686
+
687
+ it ( 'on task error, we should clear timeout' , async ( ) => {
688
+ jest . spyOn ( global , 'clearTimeout' ) ;
689
+
690
+ const taskId = 'task1' ;
691
+ const runnerId = 'runner1' ;
692
+ const requesterId = 'requester1' ;
693
+ const requesterCallback = jest . fn ( ) ;
694
+
695
+ taskBroker . registerRequester ( requesterId , requesterCallback ) ;
696
+ taskBroker . setTasks ( {
697
+ [ taskId ] : {
698
+ id : taskId ,
699
+ runnerId,
700
+ requesterId,
701
+ taskType : 'test' ,
702
+ timeout : setTimeout ( ( ) => { } , config . taskTimeout * Time . seconds . toMilliseconds ) ,
703
+ } ,
704
+ } ) ;
705
+
706
+ await taskBroker . taskErrorHandler ( taskId , new Error ( 'Test error' ) ) ;
707
+
708
+ expect ( clearTimeout ) . toHaveBeenCalled ( ) ;
709
+ expect ( taskBroker . getTasks ( ) . get ( taskId ) ) . toBeUndefined ( ) ;
710
+ } ) ;
711
+
712
+ it ( 'on timeout, we should emit `runner:timed-out-during-task` event and send error to requester' , async ( ) => {
713
+ jest . spyOn ( global , 'clearTimeout' ) ;
714
+
715
+ const taskId = 'task1' ;
716
+ const runnerId = 'runner1' ;
717
+ const requesterId = 'requester1' ;
718
+ const runner = mock < TaskRunner > ( { id : runnerId } ) ;
719
+ const runnerCallback = jest . fn ( ) ;
720
+ const requesterCallback = jest . fn ( ) ;
721
+
722
+ taskBroker . registerRunner ( runner , runnerCallback ) ;
723
+ taskBroker . registerRequester ( requesterId , requesterCallback ) ;
724
+
725
+ taskBroker . setTasks ( {
726
+ [ taskId ] : { id : taskId , runnerId, requesterId, taskType : 'test' } ,
727
+ } ) ;
728
+
729
+ await taskBroker . sendTaskSettings ( taskId , { } ) ;
730
+
731
+ jest . runAllTimers ( ) ;
732
+
733
+ await Promise . resolve ( ) ;
734
+
735
+ expect ( runnerLifecycleEvents . emit ) . toHaveBeenCalledWith ( 'runner:timed-out-during-task' ) ;
736
+
737
+ await Promise . resolve ( ) ;
738
+
739
+ expect ( clearTimeout ) . toHaveBeenCalled ( ) ;
740
+
741
+ expect ( requesterCallback ) . toHaveBeenCalledWith ( {
742
+ type : 'broker:taskerror' ,
743
+ taskId,
744
+ error : new ApplicationError ( `Task execution timed out after ${ config . taskTimeout } seconds` ) ,
745
+ } ) ;
746
+
747
+ await Promise . resolve ( ) ;
748
+
749
+ expect ( taskBroker . getTasks ( ) . get ( taskId ) ) . toBeUndefined ( ) ;
750
+ } ) ;
751
+ } ) ;
621
752
} ) ;
0 commit comments