-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(functions): report discovery crashes instead of blaming the timeout #10911
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,6 @@ | ||
| - Added `appcheck:providers:list`, `appcheck:providers:get` and `appcheck:providers:set` to configure App Check attestation providers for an app. | ||
| - Added `appcheck:apps:list` to show every app with its configured App Check providers. | ||
| - Added web app support for Crashlytics MCP tools and prompts.>>>>>>> main | ||
| - Fixed function discovery reporting a timeout when the discovery server had actually crashed, hiding the underlying error (#7775). | ||
| - Clarified the function discovery timeout error, which now names `FUNCTIONS_DISCOVERY_TIMEOUT` and reports the elapsed time in the same unit the variable accepts. | ||
| - Fixed `FUNCTIONS_DISCOVERY_TIMEOUT` silently accepting a millisecond value as seconds. It now also accepts an explicit `s` or `ms` suffix. |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -25,6 +25,18 @@ import * as versioning from "./versioning"; | |||||||||||||||||||||
|
|
||||||||||||||||||||||
| import { fileExistsSync } from "../../../../fsutils"; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** A running function discovery server. */ | ||||||||||||||||||||||
| interface AdminServer { | ||||||||||||||||||||||
| /** Shuts the server down. Safe to call after it has already exited. */ | ||||||||||||||||||||||
| kill: () => Promise<void>; | ||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * Rejects if the server exits before discovery has finished, and otherwise | ||||||||||||||||||||||
| * never settles. Discovery races this so that a crash during module load is | ||||||||||||||||||||||
| * reported as itself rather than as a timeout. | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
| serverExited: Promise<never>; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| /** | ||||||||||||||||||||||
| * | ||||||||||||||||||||||
| */ | ||||||||||||||||||||||
|
|
@@ -255,28 +267,75 @@ export class Delegate { | |||||||||||||||||||||
| config: backend.RuntimeConfigValues, | ||||||||||||||||||||||
| envs: backend.EnvironmentVariables, | ||||||||||||||||||||||
| port: string, | ||||||||||||||||||||||
| ): Promise<() => Promise<void>> { | ||||||||||||||||||||||
| ): Promise<AdminServer> { | ||||||||||||||||||||||
| const childProcess = this.spawnFunctionsProcess(config, { ...envs, PORT: port }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // TODO: Refactor return type to () => Promise<void> to simplify nested promises | ||||||||||||||||||||||
| return Promise.resolve(async () => { | ||||||||||||||||||||||
| const p = new Promise<void>((resolve, reject) => { | ||||||||||||||||||||||
| childProcess.once("exit", resolve); | ||||||||||||||||||||||
| childProcess.once("error", reject); | ||||||||||||||||||||||
| // Attached at spawn time rather than in kill(): neither event replays, so a | ||||||||||||||||||||||
| // server that died before shutdown ran would leave a listener that can never | ||||||||||||||||||||||
| // fire and a kill() that never resolves. | ||||||||||||||||||||||
| let exited = false; | ||||||||||||||||||||||
| const exit = new Promise<void>((resolve, reject) => { | ||||||||||||||||||||||
| childProcess.once("exit", () => { | ||||||||||||||||||||||
| exited = true; | ||||||||||||||||||||||
| resolve(); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| childProcess.once("error", reject); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| exit.catch(() => { | ||||||||||||||||||||||
| // kill() is the only intended consumer; it may never be called. | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| let stderr = ""; | ||||||||||||||||||||||
| childProcess.stderr?.on("data", (chunk: Buffer) => { | ||||||||||||||||||||||
| stderr += chunk.toString(); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
Comment on lines
+288
to
+291
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Buffer
Suggested change
|
||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Armed only while discovery is in flight. Once we have asked the server to | ||||||||||||||||||||||
| // quit, an exit is expected rather than a crash. | ||||||||||||||||||||||
| let armed = true; | ||||||||||||||||||||||
| const serverExited = new Promise<never>((_resolve, reject) => { | ||||||||||||||||||||||
| childProcess.once("exit", (code, signal) => { | ||||||||||||||||||||||
| if (!armed) { | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| const how = code === null ? `signal ${String(signal)}` : `code ${code}`; | ||||||||||||||||||||||
| const details = stderr.trim(); | ||||||||||||||||||||||
| reject( | ||||||||||||||||||||||
| new FirebaseError( | ||||||||||||||||||||||
| `User code failed to load. Cannot determine backend specification. ` + | ||||||||||||||||||||||
| `The functions process exited with ${how} before it could be analyzed.` + | ||||||||||||||||||||||
| (details ? `\n\n${details}` : ""), | ||||||||||||||||||||||
| ), | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
Comment on lines
+296
to
+311
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the child process fails to start/spawn (e.g., due to a system error like const serverExited = new Promise<never>((_resolve, reject) => {
childProcess.once("exit", (code, signal) => {
if (!armed) {
return;
}
const how = code === null ? `signal ${String(signal)}` : `code ${code}`;
const details = stderr.trim();
reject(
new FirebaseError(
`User code failed to load. Cannot determine backend specification. ` +
`The functions process exited with ${how} before it could be analyzed.` +
(details ? `\\n\\n${details}` : ""),
),
);
});
childProcess.once("error", (err) => {
if (!armed) {
return;
}
reject(
new FirebaseError(
`User code failed to load. Cannot determine backend specification. ` +
`The functions process failed to start: ${err.message}`,
{ original: err },
),
);
});
}); |
||||||||||||||||||||||
| serverExited.catch(() => { | ||||||||||||||||||||||
| // Discovery may finish before the server exits and never race this. | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| const kill = async (): Promise<void> => { | ||||||||||||||||||||||
| armed = false; | ||||||||||||||||||||||
| if (exited) { | ||||||||||||||||||||||
| return; | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| await fetch(`http://localhost:${port}/__/quitquitquit`); | ||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||
| logger.debug("Failed to call quitquitquit. This often means the server failed to start", e); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| setTimeout(() => { | ||||||||||||||||||||||
| const killTimer = setTimeout(() => { | ||||||||||||||||||||||
| if (!childProcess.killed) { | ||||||||||||||||||||||
| childProcess.kill("SIGKILL"); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }, 10_000); | ||||||||||||||||||||||
| return p; | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| await exit; | ||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||
| clearTimeout(killTimer); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| return Promise.resolve({ kill, serverExited }); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // eslint-disable-next-line require-await | ||||||||||||||||||||||
|
|
@@ -314,9 +373,16 @@ export class Delegate { | |||||||||||||||||||||
| // HTTP-based discovery (default) | ||||||||||||||||||||||
| const basePort = 8000 + randomInt(0, 1000); // Add a jitter to reduce likelihood of race condition | ||||||||||||||||||||||
| const port = await portfinder.getPortPromise({ port: basePort }); | ||||||||||||||||||||||
| const kill = await this.serveAdmin(config, env, port.toString()); | ||||||||||||||||||||||
| const { kill, serverExited } = await this.serveAdmin(config, env, port.toString()); | ||||||||||||||||||||||
| try { | ||||||||||||||||||||||
| discovered = await discovery.detectFromPort(port, this.projectId, this.runtime); | ||||||||||||||||||||||
| discovered = await discovery.detectFromPort( | ||||||||||||||||||||||
| port, | ||||||||||||||||||||||
| this.projectId, | ||||||||||||||||||||||
| this.runtime, | ||||||||||||||||||||||
| undefined, | ||||||||||||||||||||||
| undefined, | ||||||||||||||||||||||
| serverExited, | ||||||||||||||||||||||
| ); | ||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||
| await kill(); | ||||||||||||||||||||||
| } | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use the existing
sleeputility fromsrc/utils.tsinstead of redefining it, as per the repository style guide.References