Skip to content

Commit 79296aa

Browse files
ParidelPooyaPooya Paridel
andcommitted
fix: prevent worker process hanging with proper timer cleanup (#13)
fix: prevent worker process hanging with proper timer cleanup Fixes 'A worker process has failed to exit gracefully' error by ensuring all setTimeout timers are properly cleaned up. ## Root Cause: - waitBeforeContinue creates multiple polling timers via setTimeout - Promise.race resolves when any condition is met - Remaining timers from unresolved promises kept running - Jest worker processes couldn't exit due to active handles ## Solution: - Track all setTimeout timers in arrays - Clear all timers after Promise.race resolves - Add proper cleanup in both implementation and tests - Prevent resource leaks and hanging processes ## Changes: - waitBeforeContinue: Track and cleanup timers after Promise.race - Tests: Track test timers and clear in afterEach hooks - Eliminates worker process hanging warnings - Ensures proper resource management in production Co-authored-by: Pooya Paridel <parpooya@amazon.com>
1 parent e1e4bd5 commit 79296aa

File tree

2 files changed

+26
-6
lines changed

2 files changed

+26
-6
lines changed

packages/lambda-durable-functions-sdk-js/src/utils/wait-before-continue/wait-before-continue.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,20 @@ import { OperationStatus, Operation } from '@amzn/dex-internal-sdk';
55
describe('waitBeforeContinue', () => {
66
let mockContext: jest.Mocked<ExecutionContext>;
77
let mockHasRunningOperations: jest.Mock;
8+
let timers: NodeJS.Timeout[] = [];
89

910
beforeEach(() => {
1011
mockContext = {
1112
getStepData: jest.fn(),
1213
} as any;
1314
mockHasRunningOperations = jest.fn();
15+
timers = [];
16+
});
17+
18+
afterEach(() => {
19+
// Clean up any remaining timers
20+
timers.forEach(timer => clearTimeout(timer));
21+
timers = [];
1422
});
1523

1624
test('should resolve when operations complete', async () => {
@@ -28,9 +36,10 @@ describe('waitBeforeContinue', () => {
2836
});
2937

3038
// Complete operations after 50ms
31-
setTimeout(() => {
39+
const timer = setTimeout(() => {
3240
operationsRunning = false;
3341
}, 50);
42+
timers.push(timer);
3443

3544
const result = await resultPromise;
3645
expect(result.reason).toBe('operations');
@@ -68,9 +77,10 @@ describe('waitBeforeContinue', () => {
6877
});
6978

7079
// Change status after 50ms
71-
setTimeout(() => {
80+
const timer = setTimeout(() => {
7281
stepStatus = OperationStatus.SUCCEEDED;
7382
}, 50);
83+
timers.push(timer);
7484

7585
const result = await resultPromise;
7686
expect(result.reason).toBe('status');

packages/lambda-durable-functions-sdk-js/src/utils/wait-before-continue/wait-before-continue.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,13 +54,20 @@ export async function waitBeforeContinue(
5454
} = options;
5555

5656
const promises: Promise<WaitBeforeContinueResult>[] = [];
57+
const timers: NodeJS.Timeout[] = [];
58+
59+
// Cleanup function to clear all timers
60+
const cleanup = () => {
61+
timers.forEach(timer => clearTimeout(timer));
62+
};
5763

5864
// Timer promise - resolves when scheduled time is reached
5965
if (checkTimer && scheduledTimestamp) {
6066
const timerPromise = new Promise<WaitBeforeContinueResult>(resolve => {
6167
const timeLeft = Number(scheduledTimestamp) - Date.now();
6268
if (timeLeft > 0) {
63-
setTimeout(() => resolve({ reason: 'timer', timerExpired: true }), timeLeft);
69+
const timer = setTimeout(() => resolve({ reason: 'timer', timerExpired: true }), timeLeft);
70+
timers.push(timer);
6471
} else {
6572
resolve({ reason: 'timer', timerExpired: true });
6673
}
@@ -75,7 +82,8 @@ export async function waitBeforeContinue(
7582
if (!hasRunningOperations()) {
7683
resolve({ reason: 'operations' });
7784
} else {
78-
setTimeout(checkOperations, pollingInterval);
85+
const timer = setTimeout(checkOperations, pollingInterval);
86+
timers.push(timer);
7987
}
8088
};
8189
checkOperations();
@@ -92,7 +100,8 @@ export async function waitBeforeContinue(
92100
if (originalStatus !== currentStatus) {
93101
resolve({ reason: 'status' });
94102
} else {
95-
setTimeout(checkStepStatus, pollingInterval);
103+
const timer = setTimeout(checkStepStatus, pollingInterval);
104+
timers.push(timer);
96105
}
97106
};
98107
checkStepStatus();
@@ -105,8 +114,9 @@ export async function waitBeforeContinue(
105114
return { reason: 'timeout' };
106115
}
107116

108-
// Wait for any condition to be met
117+
// Wait for any condition to be met, then cleanup timers
109118
const result = await Promise.race(promises);
119+
cleanup();
110120

111121
// If timer expired, force checkpoint to get fresh data from API
112122
if (result.reason === 'timer' && result.timerExpired && checkpoint) {

0 commit comments

Comments
 (0)