@@ -7,12 +7,15 @@ import {
77 TaskMetadataWithFunctions ,
88 TaskRunErrorCodes ,
99 TaskRunExecution ,
10+ TaskRunExecutionResult ,
11+ TaskRunExecutionRetry ,
1012} from "../src/v3/index.js" ;
1113import { TracingSDK } from "../src/v3/otel/tracingSDK.js" ;
1214import { TriggerTracer } from "../src/v3/tracer.js" ;
1315import { TaskExecutor } from "../src/v3/workers/taskExecutor.js" ;
1416import { StandardLifecycleHooksManager } from "../src/v3/lifecycleHooks/manager.js" ;
1517import { lifecycleHooks } from "../src/v3/index.js" ;
18+ import { ApiError } from "../src/v3/apiClient/errors.js" ;
1619
1720describe ( "TaskExecutor" , ( ) => {
1821 beforeEach ( ( ) => {
@@ -1664,6 +1667,150 @@ describe("TaskExecutor", () => {
16641667 } ,
16651668 } ) ;
16661669 } ) ;
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+ } ) ;
16671814} ) ;
16681815
16691816function executeTask (
0 commit comments