Skip to content

Commit 96ec507

Browse files
refactor!: move function wrapping earlier
This commit extracts the logic for wrapping user functions into its own module and adds unit tests. It also refactors the code path that wraps the user function earlier in the initialization logic. This removes registration from the critical request path.
1 parent 5856009 commit 96ec507

File tree

5 files changed

+368
-230
lines changed

5 files changed

+368
-230
lines changed

src/function_wrappers.ts

Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// Copyright 2021 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Node.js server that runs user's code on HTTP request. HTTP response is sent
16+
// once user's function has completed.
17+
// The server accepts following HTTP requests:
18+
// - POST '/*' for executing functions (only for servers handling functions
19+
// with non-HTTP trigger).
20+
// - ANY (all methods) '/*' for executing functions (only for servers handling
21+
// functions with HTTP trigger).
22+
23+
// eslint-disable-next-line node/no-deprecated-api
24+
import * as domain from 'domain';
25+
import {Request, Response, RequestHandler} from 'express';
26+
import {sendCrashResponse} from './logger';
27+
import {sendResponse} from './invoker';
28+
import {isBinaryCloudEvent, getBinaryCloudEventContext} from './cloudevents';
29+
import {
30+
HttpFunction,
31+
EventFunction,
32+
EventFunctionWithCallback,
33+
Context,
34+
CloudEventFunction,
35+
CloudEventFunctionWithCallback,
36+
CloudEventsContext,
37+
HandlerFunction,
38+
} from './functions';
39+
import {SignatureType} from './types';
40+
41+
/**
42+
* The handler function used to signal completion of event functions.
43+
*/
44+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
45+
type OnDoneCallback = (err: Error | null, result: any) => void;
46+
47+
/**
48+
* Get a completion handler that can be used to signal completion of an event function.
49+
* @param res the response object of the request the completion handler is for.
50+
* @returns an OnDoneCallback for the provided request.
51+
*/
52+
const getOnDoneCallback = (res: Response): OnDoneCallback => {
53+
return process.domain.bind<OnDoneCallback>((err, result) => {
54+
if (res.locals.functionExecutionFinished) {
55+
console.log('Ignoring extra callback call');
56+
} else {
57+
res.locals.functionExecutionFinished = true;
58+
if (err) {
59+
console.error(err.stack);
60+
}
61+
sendResponse(result, err, res);
62+
}
63+
});
64+
};
65+
66+
/**
67+
* Helper function to parse a cloudevent object from an HTTP request.
68+
* @param req an Express HTTP request
69+
* @returns a cloudevent parsed from the request
70+
*/
71+
const parseCloudEventRequest = (req: Request): CloudEventsContext => {
72+
let cloudevent = req.body;
73+
if (isBinaryCloudEvent(req)) {
74+
cloudevent = getBinaryCloudEventContext(req);
75+
cloudevent.data = req.body;
76+
}
77+
return cloudevent;
78+
};
79+
80+
/**
81+
* Helper function to background event context and data payload object from an HTTP
82+
* request.
83+
* @param req an Express HTTP request
84+
* @returns the data playload and event context parsed from the request
85+
*/
86+
const parseBackgroundEvent = (req: Request): {data: {}; context: Context} => {
87+
const event = req.body;
88+
const data = event.data;
89+
let context = event.context;
90+
if (context === undefined) {
91+
// Support legacy events and CloudEvents in structured content mode, with
92+
// context properties represented as event top-level properties.
93+
// Context is everything but data.
94+
context = event;
95+
// Clear the property before removing field so the data object
96+
// is not deleted.
97+
context.data = undefined;
98+
delete context.data;
99+
}
100+
return {data, context};
101+
};
102+
103+
/**
104+
* Wraps the provided function into an Express handler function with additional
105+
* instrumentation logic.
106+
* @param execute Runs user's function.
107+
* @return An Express handler function.
108+
*/
109+
const wrapHttpFunction = (execute: HttpFunction): RequestHandler => {
110+
return (req: Request, res: Response) => {
111+
const d = domain.create();
112+
// Catch unhandled errors originating from this request.
113+
d.on('error', err => {
114+
if (res.locals.functionExecutionFinished) {
115+
console.error(`Exception from a finished function: ${err}`);
116+
} else {
117+
res.locals.functionExecutionFinished = true;
118+
sendCrashResponse({err, res});
119+
}
120+
});
121+
d.run(() => {
122+
process.nextTick(() => {
123+
execute(req, res);
124+
});
125+
});
126+
};
127+
};
128+
129+
/**
130+
* Wraps an async cloudevent function in an express RequestHandler.
131+
* @param userFunction User's function.
132+
* @return An Express hander function that invokes the user function.
133+
*/
134+
const wrapCloudEventFunction = (
135+
userFunction: CloudEventFunction
136+
): RequestHandler => {
137+
const httpHandler = (req: Request, res: Response) => {
138+
const callback = getOnDoneCallback(res);
139+
const cloudevent = parseCloudEventRequest(req);
140+
Promise.resolve()
141+
.then(() => userFunction(cloudevent))
142+
.then(
143+
result => callback(null, result),
144+
err => callback(err, undefined)
145+
);
146+
};
147+
return wrapHttpFunction(httpHandler);
148+
};
149+
150+
/**
151+
* Wraps callback style cloudevent function in an express RequestHandler.
152+
* @param userFunction User's function.
153+
* @return An Express hander function that invokes the user function.
154+
*/
155+
const wrapCloudEventFunctionWithCallback = (
156+
userFunction: CloudEventFunctionWithCallback
157+
): RequestHandler => {
158+
const httpHandler = (req: Request, res: Response) => {
159+
const callback = getOnDoneCallback(res);
160+
const cloudevent = parseCloudEventRequest(req);
161+
return userFunction(cloudevent, callback);
162+
};
163+
return wrapHttpFunction(httpHandler);
164+
};
165+
166+
/**
167+
* Wraps an async event function in an express RequestHandler.
168+
* @param userFunction User's function.
169+
* @return An Express hander function that invokes the user function.
170+
*/
171+
const wrapEventFunction = (userFunction: EventFunction): RequestHandler => {
172+
const httpHandler = (req: Request, res: Response) => {
173+
const callback = getOnDoneCallback(res);
174+
const {data, context} = parseBackgroundEvent(req);
175+
Promise.resolve()
176+
.then(() => userFunction(data, context))
177+
.then(
178+
result => callback(null, result),
179+
err => callback(err, undefined)
180+
);
181+
};
182+
return wrapHttpFunction(httpHandler);
183+
};
184+
185+
/**
186+
* Wraps an callback style event function in an express RequestHandler.
187+
* @param userFunction User's function.
188+
* @return An Express hander function that invokes the user function.
189+
*/
190+
const wrapEventFunctionWithCallback = (
191+
userFunction: EventFunctionWithCallback
192+
): RequestHandler => {
193+
const httpHandler = (req: Request, res: Response) => {
194+
const callback = getOnDoneCallback(res);
195+
const {data, context} = parseBackgroundEvent(req);
196+
return userFunction(data, context, callback);
197+
};
198+
return wrapHttpFunction(httpHandler);
199+
};
200+
201+
/**
202+
* Wraps a user function with the provided signature type in an express
203+
* RequestHandler.
204+
* @param userFunction User's function.
205+
* @return An Express hander function that invokes the user function.
206+
*/
207+
export const wrapUserFunction = (
208+
userFunction: HandlerFunction,
209+
signatureType: SignatureType
210+
): RequestHandler => {
211+
switch (signatureType) {
212+
case 'http':
213+
return wrapHttpFunction(userFunction as HttpFunction);
214+
case 'event':
215+
// Callback style if user function has more than 2 arguments.
216+
if (userFunction!.length > 2) {
217+
return wrapEventFunctionWithCallback(
218+
userFunction as EventFunctionWithCallback
219+
);
220+
}
221+
return wrapEventFunction(userFunction as EventFunction);
222+
case 'cloudevent':
223+
if (userFunction!.length > 1) {
224+
// Callback style if user function has more than 1 argument.
225+
return wrapCloudEventFunctionWithCallback(
226+
userFunction as CloudEventFunctionWithCallback
227+
);
228+
}
229+
return wrapCloudEventFunction(userFunction as CloudEventFunction);
230+
}
231+
};

0 commit comments

Comments
 (0)