This repository was archived by the owner on Apr 18, 2024. It is now read-only.
forked from anomalyco/sst
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbootstrap.js
More file actions
236 lines (202 loc) · 7.08 KB
/
Copy pathbootstrap.js
File metadata and controls
236 lines (202 loc) · 7.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
/**
* Based on https://github.com/lambci/node-custom-lambda/blob/0e3f2133bb2b667fa29aa4adfc30fab22166f6e4/v10.x/bootstrap.js
*/
"use strict";
require("source-map-support").install();
process.on("unhandledRejection", (err) => {
throw err;
});
const path = require("path");
const fetch = require("node-fetch");
const { getChildLogger, initializeLogger } = require("@serverless-stack/core");
const { serializeError } = require("../../lib/serializeError");
const CALLBACK_USED = Symbol("CALLBACK_USED");
const CALLBACK_IS_INVOKING = Symbol("CALLBACK_IS_INVOKING");
const ASYNC_HANDLER = Symbol("ASYNC_HANDLER");
const EXIT_ON_CALLBACK = Symbol("EXIT_ON_CALLBACK");
const argv = process.argv.slice(2);
let EVENT;
let CONTEXT;
let TIMEOUT_AT;
const TASK_ROOT = argv[0];
const HANDLER = argv[1];
const ORIG_HANDLER_PATH = argv[2];
const APP_BUILD_PATH = argv[3];
// Configure logger
initializeLogger(APP_BUILD_PATH);
const logger = getChildLogger("lambda");
start();
async function start() {
let handler;
try {
await fetchRequest();
handler = getHandler();
} catch (e) {
logger.debug("caught getHandler error");
return invokeErrorAndExit(e);
}
processEvents(handler);
}
async function fetchRequest() {
logger.debug("fetchRequest");
const url = `http://${process.env.AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next`;
const response = await fetch(url);
const headers = response.headers.raw();
EVENT = await response.json();
CONTEXT = {
invokedFunctionArn: headers["lambda-runtime-invoked-function-arn"],
awsRequestId: headers["lambda-runtime-aws-request-id"],
identity: JSON.parse(headers["lambda-runtime-cognito-identity"]),
clientContext: JSON.parse(headers["lambda-runtime-client-context"]),
};
TIMEOUT_AT = headers["lambda-runtime-deadline-ms"];
}
async function processEvents(handler) {
logger.debug("processEvents");
// Behavior of real Lambda functions with ASYNC handler:
// - on function return, the execution is done;
// - callbackWaitsForEmptyEventLoop is NOT used
//
// Behavior of real Lambda functions with SYNC handler:
// - if function returned + callback called => callback value
// - if function returned + callback NOT called => null
// - if function returned + callback will call => wait + callback value
// - if function returned + callback called + pending event loop
// + callbackWaitsForEmptyEventLoop TRUE => wait + callback value
// - if function returned + callback called + pending event loop
// + callbackWaitsForEmptyEventLoop FALSE => callback value
let result;
try {
result = await handler(EVENT, CONTEXT);
} catch (e) {
logger.debug("processEvents caught error");
return invokeErrorAndExit(e);
}
// async handler
if (CONTEXT[ASYNC_HANDLER] === true) {
logger.debug("processEvents async handler => exit 0");
return invokeResponse(result, () => process.exit(0));
}
// sync handler w/ callback called
else if (CONTEXT[CALLBACK_USED] === true) {
logger.debug("processEvents sync handler + callback used");
// not waiting for event loop => exit
if (CONTEXT.callbackWaitsForEmptyEventLoop === false) {
// Handle the case where callback was invoked, but it is still sending
// response to the parent process because process.send() is async. If
// this is the case, we will exit after the sending is completed.
if (CONTEXT[CALLBACK_IS_INVOKING]) {
logger.debug(
"callbackWaitsForEmptyEventLoop false => set EXIT_ON_CALLBACK"
);
CONTEXT[EXIT_ON_CALLBACK] = true;
} else {
logger.debug("callbackWaitsForEmptyEventLoop false => exit 0");
process.exit(0);
}
} else {
logger.debug("callbackWaitsForEmptyEventLoop true");
}
}
// sync handler w/ callback NOT called
// ie. setTimeout(() => callback(..), 1000)
else {
logger.debug("processEvents sync handler + callback NOT used");
// The handler function is not async, and the callback has NOT been called. We will send back
// a null response first, in the case the callback never gets called.
// ie. Lambda would return a null response for a sync handler if the callback does not get called.
invokeResponse(null);
// callback has not been called, exit when it gets called
if (CONTEXT.callbackWaitsForEmptyEventLoop === false) {
logger.debug("callbackWaitsForEmptyEventLoop false");
CONTEXT[EXIT_ON_CALLBACK] = true;
} else {
logger.debug("callbackWaitsForEmptyEventLoop true");
}
}
}
function getHandler() {
logger.debug("getHandler");
const app = require(path.resolve(TASK_ROOT));
const handlerName = HANDLER;
const userHandler = app[handlerName];
const origHandlerPath = ORIG_HANDLER_PATH;
if (userHandler == null) {
logger.debug("getHandler missing");
throw new Error(`Handler "${handlerName}" missing in "${origHandlerPath}"`);
} else if (typeof userHandler !== "function") {
logger.debug("getHandler not function");
throw new Error(
`Handler "${handlerName}" in "${origHandlerPath}" is not a function`
);
}
return (event, context) =>
new Promise((resolve, reject) => {
context.succeed = resolve;
context.fail = reject;
context.done = (err, data) => (err ? reject(err) : resolve(data));
context.getRemainingTimeInMillis = () => TIMEOUT_AT - Date.now();
const callback = (err, data) => {
logger.debug("callback called");
logger.debug("callback error", err);
logger.debug("callback data", data);
context[CALLBACK_USED] = true;
context.done(err, data);
context[CALLBACK_IS_INVOKING] = true;
invokeResponse(data, () => {
context[CALLBACK_IS_INVOKING] = false;
// EXIT_ON_CALLBACK is called when the handler has returned, but callback
// has not been called. Also the callbackWaitsForEmptyEventLoop is set
// to FALSE
if (context[EXIT_ON_CALLBACK] === true) {
logger.debug("callback EXIT_ON_CALLBACK set => exit 0");
process.exit(0);
}
});
};
logger.debug("runHandler");
let result;
try {
result = userHandler(event, context, callback);
} catch (e) {
return reject(e);
}
// returned a Promise
if (result != null && typeof result.then === "function") {
context[ASYNC_HANDLER] = true;
result.then(resolve, reject);
}
// returned a non-Promise
else {
return resolve();
}
});
}
function invokeResponse(result, cb) {
const ts = Date.now();
logger.debug(`invokeResponse [${ts}] started`, result);
process.send(
{
type: "success",
data: result === undefined ? null : result,
},
() => {
logger.debug(`invokeResponse [${ts}] completed`);
cb && cb();
}
);
}
function invokeErrorAndExit(err) {
const ts = Date.now();
logger.debug(`invokeError [${ts}] started`, err);
process.send(
{
type: "failure",
error: serializeError(err),
},
() => {
logger.debug(`invokeError [${ts}] completed`);
process.exit(1);
}
);
}