Skip to content

Commit af864b7

Browse files
committed
test: add e2e tests for retry handler undefined/null return fix
Add 4 Azure Managed e2e tests validating that custom retry handlers with undefined/null returns fail the task instead of retrying infinitely: - retry handler returns undefined -> task fails (not infinite retry) - retry handler returns null -> task fails (not infinite retry) - retry handler returns true -> retries and succeeds - retry handler returns positive number -> retries with delay
1 parent f2112c4 commit af864b7

1 file changed

Lines changed: 138 additions & 0 deletions

File tree

test/e2e-azuremanaged/orchestration.spec.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -657,6 +657,144 @@ describe("Durable Task Scheduler (DTS) E2E Tests", () => {
657657
expect(invoked).toBe(true);
658658
}, 31000);
659659

660+
// ==================== Retry Handler Tests ====================
661+
662+
it("should fail (not retry infinitely) when retry handler returns undefined", async () => {
663+
// Issue: A retry handler with a missing return statement returns undefined.
664+
// Before the fix, `undefined !== false` was truthy, so the executor treated it
665+
// as "retry", causing an infinite retry loop. The fix uses a positive check:
666+
// only `true` or a finite number triggers a retry.
667+
let attemptCount = 0;
668+
669+
const failingActivity = async (_: ActivityContext) => {
670+
attemptCount++;
671+
throw new Error(`Failure on attempt ${attemptCount}`);
672+
};
673+
674+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any {
675+
// Cast to any to simulate a JavaScript consumer or handler with a missing return
676+
const retryHandler = (async (_retryCtx: any) => {
677+
// Intentionally no return statement — returns undefined
678+
}) as any;
679+
const result = yield ctx.callActivity(failingActivity, undefined, { retry: retryHandler });
680+
return result;
681+
};
682+
683+
taskHubWorker.addActivity(failingActivity);
684+
taskHubWorker.addOrchestrator(orchestrator);
685+
await taskHubWorker.start();
686+
687+
const id = await taskHubClient.scheduleNewOrchestration(orchestrator);
688+
const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30);
689+
690+
expect(state).toBeDefined();
691+
// Should fail after exactly 1 attempt — the handler returned undefined so no retry
692+
expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_FAILED);
693+
expect(state?.failureDetails).toBeDefined();
694+
expect(attemptCount).toBe(1);
695+
}, 31000);
696+
697+
it("should fail (not retry infinitely) when retry handler returns null", async () => {
698+
let attemptCount = 0;
699+
700+
const failingActivity = async (_: ActivityContext) => {
701+
attemptCount++;
702+
throw new Error(`Failure on attempt ${attemptCount}`);
703+
};
704+
705+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any {
706+
const retryHandler = async (_retryCtx: any) => {
707+
return null as any; // Explicitly returning null
708+
};
709+
const result = yield ctx.callActivity(failingActivity, undefined, { retry: retryHandler });
710+
return result;
711+
};
712+
713+
taskHubWorker.addActivity(failingActivity);
714+
taskHubWorker.addOrchestrator(orchestrator);
715+
await taskHubWorker.start();
716+
717+
const id = await taskHubClient.scheduleNewOrchestration(orchestrator);
718+
const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30);
719+
720+
expect(state).toBeDefined();
721+
expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_FAILED);
722+
expect(state?.failureDetails).toBeDefined();
723+
expect(attemptCount).toBe(1);
724+
}, 31000);
725+
726+
it("should retry and succeed when retry handler returns true", async () => {
727+
let attemptCount = 0;
728+
729+
const flakyActivity = async (_: ActivityContext, input: number) => {
730+
attemptCount++;
731+
if (attemptCount < 3) {
732+
throw new Error(`Transient failure on attempt ${attemptCount}`);
733+
}
734+
return input * 2;
735+
};
736+
737+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext, input: number): any {
738+
const retryHandler = async (retryCtx: any) => retryCtx.lastAttemptNumber < 5;
739+
const result = yield ctx.callActivity(flakyActivity, input, { retry: retryHandler });
740+
return result;
741+
};
742+
743+
taskHubWorker.addActivity(flakyActivity);
744+
taskHubWorker.addOrchestrator(orchestrator);
745+
await taskHubWorker.start();
746+
747+
const id = await taskHubClient.scheduleNewOrchestration(orchestrator, 21);
748+
const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30);
749+
750+
expect(state).toBeDefined();
751+
expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED);
752+
expect(state?.failureDetails).toBeUndefined();
753+
expect(state?.serializedOutput).toEqual(JSON.stringify(42));
754+
expect(attemptCount).toBe(3);
755+
}, 31000);
756+
757+
it("should retry with delay when retry handler returns a positive number", async () => {
758+
let attemptCount = 0;
759+
const attemptTimes: number[] = [];
760+
761+
const flakyActivity = async (_: ActivityContext) => {
762+
attemptCount++;
763+
attemptTimes.push(Date.now());
764+
if (attemptCount < 2) {
765+
throw new Error(`Transient failure on attempt ${attemptCount}`);
766+
}
767+
return "success";
768+
};
769+
770+
const orchestrator: TOrchestrator = async function* (ctx: OrchestrationContext): any {
771+
const retryHandler = async (_retryCtx: any) => {
772+
return 1000; // Retry after 1 second
773+
};
774+
const result = yield ctx.callActivity(flakyActivity, undefined, { retry: retryHandler });
775+
return result;
776+
};
777+
778+
taskHubWorker.addActivity(flakyActivity);
779+
taskHubWorker.addOrchestrator(orchestrator);
780+
await taskHubWorker.start();
781+
782+
const id = await taskHubClient.scheduleNewOrchestration(orchestrator);
783+
const state = await taskHubClient.waitForOrchestrationCompletion(id, undefined, 30);
784+
785+
expect(state).toBeDefined();
786+
expect(state?.runtimeStatus).toEqual(OrchestrationStatus.ORCHESTRATION_STATUS_COMPLETED);
787+
expect(state?.failureDetails).toBeUndefined();
788+
expect(state?.serializedOutput).toEqual(JSON.stringify("success"));
789+
expect(attemptCount).toBe(2);
790+
791+
// Verify there was at least ~1s delay between attempts
792+
if (attemptTimes.length >= 2) {
793+
const delay = attemptTimes[1] - attemptTimes[0];
794+
expect(delay).toBeGreaterThanOrEqual(900); // Allow some tolerance
795+
}
796+
}, 31000);
797+
660798
// // ==================== newGuid Tests ====================
661799

662800
it("should generate deterministic GUIDs with newGuid", async () => {

0 commit comments

Comments
 (0)