Skip to content

Commit 0020354

Browse files
taeoldchristhompsongoogle
authored andcommitted
Goodbye invokeTrigger. Everything is http (unless in debug mode) (#4886)
This is it! It was all for this! Now that all functions, regardless of signature, is invoked via an HTTP endpoint, we complete the refactor to remove all traces of IPC comm between the functions runtime and the functions emulator. Instead of relying on inter-process messages from the runtime process to monitor the state of the function ("is it idle? is it processing request? is it done processing the request?") we simply let the Functions Emulator submit a request to the a worker associated with the trigger and infer status based on the state of the request. The only way IPC is being used is when Functions Emulator is running in DEBUG mode. In debug mode, a single process is used to invoke all function triggers, and we use IPC to let the runtime process know which trigger should be called in the subsequent request. We'll tackle fixing this feature later - we can just let runtime for other language not support debug mode until we have a concrete plan on how we can support debug mode in the SDK itself.
1 parent a75f871 commit 0020354

File tree

9 files changed

+258
-276
lines changed

9 files changed

+258
-276
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
- Fix Storage Emulator crashing with NoClassDefFoundError in some cases (#3481).
2+
- Refactor mechanism for invoking function triggers (#4886).

npm-shrinkwrap.json

Lines changed: 60 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@
214214
"google-discovery-to-swagger": "^2.1.0",
215215
"mocha": "^9.1.3",
216216
"nock": "^13.0.5",
217+
"node-mocks-http": "^1.11.0",
217218
"nyc": "^15.1.0",
218219
"openapi-merge": "^1.0.23",
219220
"prettier": "^2.5.1",

scripts/emulator-tests/functionsEmulatorRuntime.spec.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -179,12 +179,8 @@ async function sendReq(runtime: Runtime, opts: ReqOpts = {}): Promise<string> {
179179
}
180180

181181
async function sendDebugBundle(runtime: Runtime, debug: FunctionsRuntimeBundle["debug"]) {
182-
const frb: FunctionsRuntimeBundle = {
183-
proto: {},
184-
debug,
185-
};
186182
return new Promise((resolve) => {
187-
runtime.proc.send(JSON.stringify({ frb }), resolve);
183+
runtime.proc.send(JSON.stringify(debug), resolve);
188184
});
189185
}
190186

src/emulator/functionsEmulator.ts

Lines changed: 31 additions & 127 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,7 @@ import { Account } from "../auth";
1212
import { logger } from "../logger";
1313
import { track, trackEmulator } from "../track";
1414
import { Constants } from "./constants";
15-
import {
16-
EmulatorInfo,
17-
EmulatorInstance,
18-
EmulatorLog,
19-
Emulators,
20-
FunctionsExecutionMode,
21-
} from "./types";
15+
import { EmulatorInfo, EmulatorInstance, Emulators, FunctionsExecutionMode } from "./types";
2216
import * as chokidar from "chokidar";
2317

2418
import * as spawn from "cross-spawn";
@@ -29,7 +23,6 @@ import {
2923
EventSchedule,
3024
EventTrigger,
3125
formatHost,
32-
FunctionsRuntimeBundle,
3326
FunctionsRuntimeFeatures,
3427
getFunctionService,
3528
getSignatureType,
@@ -331,7 +324,13 @@ export class FunctionsEmulator implements EmulatorInstance {
331324
return hub;
332325
}
333326

334-
async sendRequest(worker: RuntimeWorker, body?: any) {
327+
async sendRequest(trigger: EmulatedTriggerDefinition, body?: any) {
328+
const record = this.getTriggerRecordByKey(this.getTriggerKey(trigger));
329+
const pool = this.workerPools[record.backend.codebase];
330+
if (!pool.readyForWork(trigger.id)) {
331+
await this.startRuntime(record.backend, trigger);
332+
}
333+
const worker = pool.getIdleWorker(trigger.id)!;
335334
const reqBody = JSON.stringify(body);
336335
const headers = {
337336
"Content-Type": "application/json",
@@ -352,35 +351,6 @@ export class FunctionsEmulator implements EmulatorInstance {
352351
});
353352
}
354353

355-
async invokeTrigger(
356-
trigger: EmulatedTriggerDefinition,
357-
proto?: any,
358-
runtimeOpts?: InvokeRuntimeOpts
359-
): Promise<RuntimeWorker> {
360-
const record = this.getTriggerRecordByKey(this.getTriggerKey(trigger));
361-
const backend = record.backend;
362-
const bundleTemplate = this.getBaseBundle();
363-
const runtimeBundle: FunctionsRuntimeBundle = {
364-
...bundleTemplate,
365-
proto,
366-
};
367-
if (this.args.debugPort) {
368-
runtimeBundle.debug = {
369-
functionTarget: trigger.entryPoint,
370-
functionSignature: getSignatureType(trigger),
371-
};
372-
}
373-
if (!backend.nodeBinary) {
374-
throw new FirebaseError(`No node binary for ${trigger.id}. This should never happen.`);
375-
}
376-
const opts = runtimeOpts || {
377-
nodeBinary: backend.nodeBinary,
378-
extensionTriggers: backend.predefinedTriggers,
379-
};
380-
const worker = await this.invokeRuntime(backend, trigger, runtimeBundle, opts);
381-
return worker;
382-
}
383-
384354
async start(): Promise<void> {
385355
for (const backend of this.args.emulatableBackends) {
386356
backend.nodeBinary = this.getNodeBinary(backend);
@@ -680,7 +650,7 @@ export class FunctionsEmulator implements EmulatorInstance {
680650
// In debug mode, we eagerly start a runtime process to allow debuggers to attach
681651
// before invoking a function.
682652
if (this.args.debugPort) {
683-
await this.startRuntime(emulatableBackend, { nodeBinary: emulatableBackend.nodeBinary });
653+
await this.startRuntime(emulatableBackend);
684654
}
685655
}
686656

@@ -998,13 +968,6 @@ export class FunctionsEmulator implements EmulatorInstance {
998968
triggers.forEach((def) => this.addTriggerRecord(def, { backend, ignored: false }));
999969
}
1000970

1001-
getBaseBundle(): FunctionsRuntimeBundle {
1002-
return {
1003-
proto: {},
1004-
disabled_features: this.args.disabledRuntimeFeatures,
1005-
};
1006-
}
1007-
1008971
getNodeBinary(backend: EmulatableBackend): string {
1009972
const pkg = require(path.join(backend.functionsDir, "package.json"));
1010973
// If the developer hasn't specified a Node to use, inform them that it's an option and use default
@@ -1275,37 +1238,18 @@ export class FunctionsEmulator implements EmulatorInstance {
12751238
return secretEnvs;
12761239
}
12771240

1278-
async invokeRuntime(
1279-
backend: EmulatableBackend,
1280-
trigger: EmulatedTriggerDefinition,
1281-
frb: FunctionsRuntimeBundle,
1282-
opts: InvokeRuntimeOpts
1283-
): Promise<RuntimeWorker> {
1284-
const pool = this.workerPools[backend.codebase];
1285-
if (!pool.readyForWork(trigger.id)) {
1286-
await this.startRuntime(backend, opts, trigger);
1287-
}
1288-
return pool.submitWork(trigger.id, frb, opts);
1289-
}
1290-
12911241
async startRuntime(
12921242
backend: EmulatableBackend,
1293-
opts: InvokeRuntimeOpts,
12941243
trigger?: EmulatedTriggerDefinition
1295-
) {
1244+
): Promise<RuntimeWorker> {
12961245
const emitter = new EventEmitter();
12971246
const args = [path.join(__dirname, "functionsEmulatorRuntime")];
12981247

1299-
if (opts.ignore_warnings) {
1300-
args.unshift("--no-warnings");
1301-
}
1302-
13031248
if (this.args.debugPort) {
1304-
if (process.env.FIREPIT_VERSION && process.execPath === opts.nodeBinary) {
1305-
const requestedMajorNodeVersion = this.getNodeBinary(backend);
1249+
if (process.env.FIREPIT_VERSION && process.execPath === backend.nodeBinary) {
13061250
this.logger.log(
13071251
"WARN",
1308-
`To enable function inspection, please run "${process.execPath} is:npm i node@${requestedMajorNodeVersion} --save-dev" in your functions directory`
1252+
`To enable function inspection, please run "${process.execPath} is:npm i node@${backend.nodeMajorVersion} --save-dev" in your functions directory`
13091253
);
13101254
} else {
13111255
const { host } = this.getInfo();
@@ -1332,10 +1276,10 @@ export class FunctionsEmulator implements EmulatorInstance {
13321276
const secretEnvs = await this.resolveSecretEnvs(backend, trigger);
13331277
const socketPath = getTemporarySocketPath();
13341278

1335-
const childProcess = spawn(opts.nodeBinary, args, {
1279+
const childProcess = spawn(backend.nodeBinary!, args, {
13361280
cwd: backend.functionsDir,
13371281
env: {
1338-
node: opts.nodeBinary,
1282+
node: backend.nodeBinary,
13391283
...process.env,
13401284
...runtimeEnv,
13411285
...secretEnvs,
@@ -1357,7 +1301,7 @@ export class FunctionsEmulator implements EmulatorInstance {
13571301
const pool = this.workerPools[backend.codebase];
13581302
const worker = pool.addWorker(trigger?.id, runtime, extensionLogInfo);
13591303
await worker.waitForSocketReady();
1360-
return;
1304+
return worker;
13611305
}
13621306

13631307
async disableBackgroundTriggers() {
@@ -1480,20 +1424,12 @@ export class FunctionsEmulator implements EmulatorInstance {
14801424
);
14811425
}
14821426
}
1483-
const worker = await this.invokeTrigger(trigger);
1484-
14851427
// For analytics, track the invoked service
14861428
void track(EVENT_INVOKE, getFunctionService(trigger));
14871429
void trackEmulator(EVENT_INVOKE_GA4, {
14881430
function_service: getFunctionService(trigger),
14891431
});
14901432

1491-
worker.onLogs((el: EmulatorLog) => {
1492-
if (el.level === "FATAL") {
1493-
res.status(500).send(el.text);
1494-
}
1495-
});
1496-
14971433
this.logger.log("DEBUG", `[functions] Runtime ready! Sending request!`);
14981434

14991435
// To match production behavior we need to drop the path prefix
@@ -1508,59 +1444,27 @@ export class FunctionsEmulator implements EmulatorInstance {
15081444
// cause unexpected situations - not to mention CORS troubles and this enables us to use
15091445
// a socketPath (IPC socket) instead of consuming yet another port which is probably faster as well.
15101446
this.logger.log("DEBUG", `[functions] Got req.url=${req.url}, mapping to path=${path}`);
1511-
const runtimeReq = http.request(
1447+
1448+
const pool = this.workerPools[record.backend.codebase];
1449+
if (!pool.readyForWork(trigger.id)) {
1450+
await this.startRuntime(record.backend, trigger);
1451+
}
1452+
const debugBundle = this.args.debugPort
1453+
? {
1454+
functionTarget: trigger.entryPoint,
1455+
functionSignature: getSignatureType(trigger),
1456+
}
1457+
: undefined;
1458+
await pool.submitRequest(
1459+
trigger.id,
15121460
{
15131461
method,
15141462
path,
15151463
headers: req.headers,
1516-
socketPath: worker.runtime.socketPath,
15171464
},
1518-
(runtimeRes: http.IncomingMessage) => {
1519-
function forwardStatusAndHeaders(): void {
1520-
res.status(runtimeRes.statusCode || 200);
1521-
if (!res.headersSent) {
1522-
Object.keys(runtimeRes.headers).forEach((key) => {
1523-
const val = runtimeRes.headers[key];
1524-
if (val) {
1525-
res.setHeader(key, val);
1526-
}
1527-
});
1528-
}
1529-
}
1530-
1531-
runtimeRes.on("data", (buf) => {
1532-
forwardStatusAndHeaders();
1533-
res.write(buf);
1534-
});
1535-
1536-
runtimeRes.on("close", () => {
1537-
forwardStatusAndHeaders();
1538-
res.end();
1539-
});
1540-
1541-
runtimeRes.on("end", () => {
1542-
forwardStatusAndHeaders();
1543-
res.end();
1544-
});
1545-
}
1465+
res,
1466+
reqBody,
1467+
debugBundle
15461468
);
1547-
1548-
runtimeReq.on("error", () => {
1549-
res.end();
1550-
});
1551-
1552-
// If the original request had a body, forward that over the connection.
1553-
// TODO: Why is this not handled by the pipe?
1554-
if (reqBody) {
1555-
runtimeReq.write(reqBody);
1556-
runtimeReq.end();
1557-
}
1558-
1559-
// Pipe the incoming request over the socket.
1560-
req.pipe(runtimeReq, { end: true }).on("error", () => {
1561-
res.end();
1562-
});
1563-
1564-
await worker.waitForDone();
15651469
}
15661470
}

0 commit comments

Comments
 (0)