The AWS Lambda, Programming Model document states:
Your Lambda function code must be written in a stateless style, and have no affinity with the underlying compute infrastructure.
The AWS Lambda, Best Practices document recommends:
Take advantage of container re-use to improve the performance of your function.
Those contradictory statements make sense at scale.
When an AWS Lambda function completes (which means it terminates without unhandled exceptions, either with an error (also referred to as a failure) or a result (also referred to as a success)), the AWS Lambda service will automatically freeze its container and probably reuse it for a new invocation.
When an AWS Lambda function takes longer than the specified maximum execution time, the AWS Lambda service times it out by throwing an exception (unhandled by definition) and then manages it accordingly to some policies.
Given that AWS Lambda functions for a NodeJS runtime are NodeJS applications that, in general, add callbacks to the event loop, the AWS Lambda service will, by default, make sure that there is no pending work before considering the AWS Lambda function fully completed, independently from the fact that its callback was already called or not. To call the callback is, in fact, optional.
Unfortunately, if the AWS Lambda function somehow blocks the event loop (for example, a database connection which is being periodically polled), the AWS Lambda service won’t ever find the event loop empty and the AWS Lambda function will time out.
NodeJS V8 has an experimental Async Hooks module to help debug these situations, while V6 (used by AWS Lambda) has these two undocumented functions:
process._getActiveHandles()
gets you handles that are still aliveprocess._getActiveRequests()
gets you info about activelibuv
requests.
Conveniently, the AWS Lambda service calls the exports.handler
function with a second argument set to the container context. In turn the context has a callbackWaitsForEmptyEventLoop
property which can be set to false
to signal to the AWS Lambda service that the callback call (in a way) marks the end of the AWS Lambda function.
AWS Lambda will freeze the process, any state data and the events in the NodeJS event loop (any remaining events in the event loop [will be] processed when the Lambda function is called next and if AWS Lambda chooses to use the frozen process).
let expensiveResult;
exports.handler = (event, context, callback) => {
endWhenTheCallbackIsCalledOrTheTimeoutIsReached(context);
process(event, callback);
};
// ---
function process(event, done) {
try {
if (! expensiveResult) {
// set expensiveResult
}
// use expensiveResult
// then
done(null, result);
} catch (e) {
done(JSON.stringify(e));
}
}
function endWhenTheCallbackIsCalledOrTheTimeoutIsReached(context) {
context.callbackWaitsForEmptyEventLoop = false;
}
function endWhenTheEventLoopIsEmptyOrTheTimeoutIsReached(context) {
context.callbackWaitsForEmptyEventLoop = true;
}