@@ -7,12 +7,15 @@ import {
7
7
TaskMetadataWithFunctions ,
8
8
TaskRunErrorCodes ,
9
9
TaskRunExecution ,
10
+ TaskRunExecutionResult ,
11
+ TaskRunExecutionRetry ,
10
12
} from "../src/v3/index.js" ;
11
13
import { TracingSDK } from "../src/v3/otel/tracingSDK.js" ;
12
14
import { TriggerTracer } from "../src/v3/tracer.js" ;
13
15
import { TaskExecutor } from "../src/v3/workers/taskExecutor.js" ;
14
16
import { StandardLifecycleHooksManager } from "../src/v3/lifecycleHooks/manager.js" ;
15
17
import { lifecycleHooks } from "../src/v3/index.js" ;
18
+ import { ApiError } from "../src/v3/apiClient/errors.js" ;
16
19
17
20
describe ( "TaskExecutor" , ( ) => {
18
21
beforeEach ( ( ) => {
@@ -1664,6 +1667,150 @@ describe("TaskExecutor", () => {
1664
1667
} ,
1665
1668
} ) ;
1666
1669
} ) ;
1670
+
1671
+ test ( "should skip retrying for unretryable API errors" , async ( ) => {
1672
+ const unretryableStatusCodes = [ 400 , 401 , 403 , 404 , 422 ] ;
1673
+ const retryableStatusCodes = [ 408 , 429 , 500 , 502 , 503 , 504 ] ;
1674
+
1675
+ // Register global init hook
1676
+ lifecycleHooks . registerGlobalInitHook ( {
1677
+ id : "test-init" ,
1678
+ fn : async ( ) => {
1679
+ return {
1680
+ foo : "bar" ,
1681
+ } ;
1682
+ } ,
1683
+ } ) ;
1684
+
1685
+ // Test each unretryable status code
1686
+ for ( const status of unretryableStatusCodes ) {
1687
+ const apiError = ApiError . generate (
1688
+ status ,
1689
+ { error : { message : "API Error" } } ,
1690
+ "API Error" ,
1691
+ { }
1692
+ ) ;
1693
+
1694
+ const task = {
1695
+ id : "test-task" ,
1696
+ fns : {
1697
+ run : async ( ) => {
1698
+ throw apiError ;
1699
+ } ,
1700
+ } ,
1701
+ retry : {
1702
+ maxAttempts : 3 ,
1703
+ minDelay : 1000 ,
1704
+ maxDelay : 5000 ,
1705
+ factor : 2 ,
1706
+ } ,
1707
+ } ;
1708
+
1709
+ const result = await executeTask ( task , { test : "data" } , undefined ) ;
1710
+
1711
+ // Verify that retrying is skipped for these status codes
1712
+ expect ( result . result ) . toMatchObject ( {
1713
+ ok : false ,
1714
+ id : "test-run-id" ,
1715
+ error : {
1716
+ type : "BUILT_IN_ERROR" ,
1717
+ message : `${ status } API Error` ,
1718
+ name : "TriggerApiError" ,
1719
+ stackTrace : expect . any ( String ) ,
1720
+ } ,
1721
+ skippedRetrying : true ,
1722
+ } ) ;
1723
+ }
1724
+
1725
+ // Test each retryable status code
1726
+ for ( const status of retryableStatusCodes ) {
1727
+ const apiError = ApiError . generate (
1728
+ status ,
1729
+ { error : { message : "API Error" } } ,
1730
+ "API Error" ,
1731
+ { }
1732
+ ) ;
1733
+
1734
+ const task = {
1735
+ id : "test-task" ,
1736
+ fns : {
1737
+ run : async ( ) => {
1738
+ throw apiError ;
1739
+ } ,
1740
+ } ,
1741
+ retry : {
1742
+ maxAttempts : 3 ,
1743
+ minDelay : 1000 ,
1744
+ maxDelay : 5000 ,
1745
+ factor : 2 ,
1746
+ } ,
1747
+ } ;
1748
+
1749
+ const result = await executeTask ( task , { test : "data" } , undefined ) ;
1750
+
1751
+ // Verify that retrying is NOT skipped for these status codes
1752
+ expect ( result . result . ok ) . toBe ( false ) ;
1753
+ expect ( result . result ) . toMatchObject ( {
1754
+ ok : false ,
1755
+ skippedRetrying : false ,
1756
+ retry : expect . objectContaining ( {
1757
+ delay : expect . any ( Number ) ,
1758
+ timestamp : expect . any ( Number ) ,
1759
+ } ) ,
1760
+ } ) ;
1761
+
1762
+ if ( status === 429 ) {
1763
+ // Rate limit errors should use the rate limit retry delay
1764
+ expect ( ( result . result as any ) . retry . delay ) . toBeGreaterThan ( 0 ) ;
1765
+ } else {
1766
+ // Other retryable errors should use the exponential backoff
1767
+ expect ( ( result . result as any ) . retry . delay ) . toBeGreaterThan ( 1000 ) ;
1768
+ expect ( ( result . result as any ) . retry . delay ) . toBeLessThan ( 5000 ) ;
1769
+ }
1770
+ }
1771
+ } ) ;
1772
+
1773
+ test ( "should respect rate limit headers for 429 errors" , async ( ) => {
1774
+ const resetTime = Date . now ( ) + 30000 ; // 30 seconds from now
1775
+ const apiError = ApiError . generate (
1776
+ 429 ,
1777
+ { error : { message : "Rate limit exceeded" } } ,
1778
+ "Rate limit exceeded" ,
1779
+ { "x-ratelimit-reset" : resetTime . toString ( ) }
1780
+ ) ;
1781
+
1782
+ const task = {
1783
+ id : "test-task" ,
1784
+ fns : {
1785
+ run : async ( ) => {
1786
+ throw apiError ;
1787
+ } ,
1788
+ } ,
1789
+ retry : {
1790
+ maxAttempts : 3 ,
1791
+ minDelay : 1000 ,
1792
+ maxDelay : 5000 ,
1793
+ factor : 2 ,
1794
+ } ,
1795
+ } ;
1796
+
1797
+ const result = await executeTask ( task , { test : "data" } , undefined ) ;
1798
+
1799
+ // Verify that the retry delay matches the rate limit reset time (with some jitter)
1800
+ expect ( result . result . ok ) . toBe ( false ) ;
1801
+ expect ( result . result ) . toMatchObject ( {
1802
+ ok : false ,
1803
+ skippedRetrying : false ,
1804
+ retry : expect . objectContaining ( {
1805
+ delay : expect . any ( Number ) ,
1806
+ timestamp : expect . any ( Number ) ,
1807
+ } ) ,
1808
+ } ) ;
1809
+
1810
+ const delay = ( result . result as any ) . retry . delay ;
1811
+ expect ( delay ) . toBeGreaterThan ( 29900 ) ; // Allow for some time passing during test
1812
+ expect ( delay ) . toBeLessThan ( 32000 ) ; // Account for max 2000ms jitter
1813
+ } ) ;
1667
1814
} ) ;
1668
1815
1669
1816
function executeTask (
0 commit comments