Skip to content

clear pending timeout after promise.race #5701

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Nov 10, 2021
Merged
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/cool-toys-marry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@firebase/functions": patch
---

Clear pending timeout after promise.race. It allows the process to exit immediately in case the SDK is used in Node.js, otherwise the process will wait for the timeout to finish before exiting.
34 changes: 27 additions & 7 deletions packages/functions/src/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,18 +53,34 @@ export interface HttpResponseBody {
};
}

interface CancellablePromise<T> {
promise: Promise<T>;
cancel: () => void;
}

/**
* Returns a Promise that will be rejected after the given duration.
* The error will be of type FunctionsError.
*
* @param millis Number of milliseconds to wait before rejecting.
*/
function failAfter(millis: number): Promise<never> {
return new Promise((_, reject) => {
setTimeout(() => {
reject(new FunctionsError('deadline-exceeded', 'deadline-exceeded'));
}, millis);
});
function failAfter(millis: number): CancellablePromise<never> {
// Node timers and browser timers are fundamentally incompatible, but we
// don't care about the value here
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let timer: any | null = null;
return {
promise: new Promise((_, reject) => {
timer = setTimeout(() => {
reject(new FunctionsError('deadline-exceeded', 'deadline-exceeded'));
}, millis);
}),
cancel: () => {
if (timer) {
clearTimeout(timer);
}
}
};
}

/**
Expand Down Expand Up @@ -247,12 +263,16 @@ async function call(
// Default timeout to 70s, but let the options override it.
const timeout = options.timeout || 70000;

const failAfterHandle = failAfter(timeout);
const response = await Promise.race([
postJSON(url, body, headers, functionsInstance.fetchImpl),
failAfter(timeout),
failAfterHandle.promise,
functionsInstance.cancelAllRequests
]);

// Always clear the failAfter timeout
failAfterHandle.cancel();

// If service was deleted, interrupted response throws an error.
if (!response) {
throw new FunctionsError(
Expand Down