Skip to content
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Refactor the way timeouts are enforced by the Functions Emulator (#5464)
29 changes: 29 additions & 0 deletions scripts/emulator-tests/functionsEmulator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1037,4 +1037,33 @@ describe("FunctionsEmulator", function () {
expect(triggerDefinitions[0].timeoutSeconds).to.equal(26);
});
});

it("should enforce timeout", async () => {
await useFunction(
emu,
"timeoutFn",
() => {
return {
timeoutFn: require("firebase-functions")
.runWith({ timeoutSeconds: 1 })
.https.onRequest((req: express.Request, res: express.Response): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => {
res.sendStatus(200);
resolve();
}, 5_000);
});
}),
};
},
["us-central1"],
{
timeoutSeconds: 1,
}
);

await supertest(emu.createHubServer())
.get("/fake-project-id/us-central1/timeoutFn")
.expect(500);
});
});
34 changes: 0 additions & 34 deletions scripts/emulator-tests/functionsEmulatorRuntime.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -730,40 +730,6 @@ describe("FunctionsEmulator-Runtime", function () {
expect(runtime.sysMsg["runtime-error"]?.length).to.eq(1);
});
});

describe("Timeout", () => {
it("enforces configured timeout", async () => {
const timeoutEnvs = {
FUNCTIONS_EMULATOR_TIMEOUT_SECONDS: "1",
FUNCTIONS_EMULATOR_DISABLE_TIMEOUT: "false",
};
runtime = await startRuntime(
"functionId",
"http",
() => {
return {
functionId: require("firebase-functions").https.onRequest(
(req: any, resp: any): Promise<void> => {
return new Promise((resolve) => {
setTimeout(() => {
resp.sendStatus(200);
resolve();
}, 5_000);
});
}
),
};
},
timeoutEnvs
);
try {
await sendReq(runtime);
} catch (e: any) {
// Carry on
}
expect(runtime.sysMsg["runtime-error"]?.length).to.eq(1);
});
});
});

describe("Debug", () => {
Expand Down
31 changes: 20 additions & 11 deletions src/deploy/functions/runtimes/python/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { runWithVirtualEnv } from "../../../../functions/python";
import { FirebaseError } from "../../../../error";
import { Build } from "../../build";

const LATEST_VERSION: runtimes.Runtime = "python310";
export const LATEST_VERSION: runtimes.Runtime = "python310";

/**
* Create a runtime delegate for the Python runtime, if applicable.
Expand All @@ -37,6 +37,24 @@ export async function tryCreateDelegate(
return Promise.resolve(new Delegate(context.projectId, context.sourceDir, runtime));
}

/**
* Get corresponding python binary name for a given runtime.
*
* By default, returns "python"
*/
export function getPythonBinary(runtime: runtimes.Runtime): string {
if (process.platform === "win32") {
// There is no easy way to get specific version of python executable in Windows.
return "python.exe";
}
if (runtime === "python310") {
return "python3.10";
} else if (runtime === "python311") {
return "python3.11";
}
return "python";
}

export class Delegate implements runtimes.RuntimeDelegate {
public readonly name = "python";
constructor(
Expand Down Expand Up @@ -82,16 +100,7 @@ export class Delegate implements runtimes.RuntimeDelegate {
}

getPythonBinary(): string {
if (process.platform === "win32") {
// There is no easy way to get specific version of python executable in Windows.
return "python.exe";
}
if (this.runtime === "python310") {
return "python3.10";
} else if (this.runtime === "python311") {
return "python3.11";
}
return "python";
return getPythonBinary(this.runtime);
}

validate(): Promise<void> {
Expand Down
8 changes: 1 addition & 7 deletions src/emulator/functionsEmulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1108,12 +1108,6 @@ export class FunctionsEmulator implements EmulatorInstance {
envs.K_REVISION = "1";
envs.PORT = "80";

// TODO(danielylee): Later, we want timeout to be enforce by the data plane. For now, we rely on the runtime to
// enforce timeout.
if (trigger?.timeoutSeconds) {
envs.FUNCTIONS_EMULATOR_TIMEOUT_SECONDS = trigger.timeoutSeconds.toString();
}

if (trigger) {
const target = trigger.entryPoint;
envs.FUNCTION_TARGET = target;
Expand Down Expand Up @@ -1357,7 +1351,7 @@ export class FunctionsEmulator implements EmulatorInstance {
};

const pool = this.workerPools[backend.codebase];
const worker = pool.addWorker(trigger?.id, runtime, extensionLogInfo);
const worker = pool.addWorker(trigger, runtime, extensionLogInfo);
await worker.waitForSocketReady();
return worker;
}
Expand Down
36 changes: 1 addition & 35 deletions src/emulator/functionsEmulatorRuntime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1018,31 +1018,13 @@ async function main(): Promise<void> {
});
app.all(`/*`, async (req: express.Request, res: express.Response) => {
try {
new EmulatorLog(
"INFO",
"runtime-status",
`Beginning execution of "${FUNCTION_TARGET_NAME}"`
).log();

const trigger = FUNCTION_TARGET_NAME.split(".").reduce((mod, functionTargetPart) => {
return mod?.[functionTargetPart];
}, functionModule) as CloudFunction<unknown>;
if (!trigger) {
throw new Error(`Failed to find function ${FUNCTION_TARGET_NAME} in the loaded module`);
}

const startHrTime = process.hrtime();
res.on("finish", () => {
const elapsedHrTime = process.hrtime(startHrTime);
new EmulatorLog(
"INFO",
"runtime-status",
`Finished "${FUNCTION_TARGET_NAME}" in ${
elapsedHrTime[0] * 1000 + elapsedHrTime[1] / 1000000
}ms`
).log();
});

switch (FUNCTION_SIGNATURE) {
case "event":
case "cloudevent":
Expand All @@ -1063,25 +1045,9 @@ async function main(): Promise<void> {
res.status(500).send(err.message);
}
});
const server = app.listen(process.env.PORT, () => {
app.listen(process.env.PORT, () => {
logDebug(`Listening to port: ${process.env.PORT}`);
});
if (!FUNCTION_DEBUG_MODE) {
let timeout = process.env.FUNCTIONS_EMULATOR_TIMEOUT_SECONDS || "60";
if (timeout.endsWith("s")) {
timeout = timeout.slice(0, -1);
}
const timeoutMs = parseInt(timeout, 10) * 1000;
server.setTimeout(timeoutMs, () => {
new EmulatorLog(
"FATAL",
"runtime-error",
`Your function timed out after ~${timeout}s. To configure this timeout, see
https://firebase.google.com/docs/functions/manage-functions#set_timeout_and_memory_allocation.`
).log();
return flushAndExit(1);
});
}

// Event emitters do not work well with async functions, so we
// construct our own promise chain to make sure each message is
Expand Down
Loading