Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/fix-bpm-close-timeout.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@aws-amplify/core': patch
---

fix(core): add timeout to BackgroundProcessManager.close() to prevent permanent stuck state
27 changes: 27 additions & 0 deletions packages/core/__tests__/BackgroundProcessManager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -664,4 +664,31 @@ describe('BackgroundProcessManager', () => {
unblock?.();
await close;
});

test('close() resolves after timeout even if jobs never complete', async () => {
const manager = new BackgroundProcessManager();
// eslint-disable-next-line @typescript-eslint/no-empty-function
const warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});

manager.add(
// eslint-disable-next-line @typescript-eslint/no-empty-function
() => new Promise<void>(() => {}),
'hanging job',
);

const start = Date.now();
await manager.close();
const elapsed = Date.now() - start;

expect(manager.isClosed).toBe(true);
expect(manager.length).toBe(0);
// should resolve around the 10s internal timeout, not hang forever
expect(elapsed).toBeLessThan(15_000);
expect(warnSpy).toHaveBeenCalledWith(
expect.stringContaining('timed out'),
expect.arrayContaining(['hanging job']),
);

warnSpy.mockRestore();
}, 20_000);
});
Original file line number Diff line number Diff line change
Expand Up @@ -389,7 +389,35 @@ export class BackgroundProcessManager {
Array.from(this.jobs).map(j => j.promise),
);

const CLOSE_TIMEOUT_MS = 10_000;
let timer: ReturnType<typeof setTimeout>;
let didTimeout = false;
const timeoutPromise = new Promise<PromiseSettledResult<any>[]>(
resolve => {
timer = setTimeout(() => {
didTimeout = true;
resolve([]);
}, CLOSE_TIMEOUT_MS);
},
);
this._closingPromise = Promise.race([
this._closingPromise,
timeoutPromise,
]);
await this._closingPromise;
clearTimeout(timer!);

if (didTimeout) {
const pendingDescriptions = this.pending.filter(Boolean);

// eslint-disable-next-line no-console
console.warn(
`BackgroundProcessManager.close() timed out after ${CLOSE_TIMEOUT_MS}ms with ${this.jobs.size} pending job(s):`,
pendingDescriptions,
);
this.jobs.clear();
}

this._state = BackgroundProcessManagerState.Closed;
}

Expand Down