Skip to content

Commit 41c9b77

Browse files
authored
feat(aws-lambda): added optional timeout detection (#1181)
refs https://jsw.ibm.com/browse/INSTA-7063 - customers need that issue for debugging purposes - env variable INSTANA_ENABLE_LAMBDA_TIMEOUT_DETECTION
1 parent abb2a77 commit 41c9b77

File tree

7 files changed

+454
-3
lines changed

7 files changed

+454
-3
lines changed

packages/aws-lambda/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
"audit": "npm audit --omit=dev",
2929
"node_modules:exists": "mkdir -p node_modules",
3030
"test": "mocha --config=test/.mocharc.js --sort $(find test -iname '*test.js')",
31-
"test:debug": "WITH_STDOUT=true npm run test",
31+
"test:debug": "WITH_STDOUT=true INSTANA_DEBUG=true npm run test",
3232
"test:ci": "mocha --config=test/.mocharc.js --reporter mocha-multi-reporters --reporter-options configFile=reporter-config.json 'test/**/*test.js'",
3333
"lint": "eslint src test lambdas",
3434
"verify": "npm run lint && npm test",

packages/aws-lambda/src/wrapper.js

+99
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,19 @@ function shimmedHandler(originalHandler, originalThis, originalArgs, _config) {
175175
});
176176
};
177177

178+
/**
179+
* We offer the customer to enable the timeout detection
180+
* But its not recommended to use it on production, only for debugging purposes.
181+
* See https://github.com/instana/nodejs/pull/668.
182+
*/
183+
if (
184+
process.env.INSTANA_ENABLE_LAMBDA_TIMEOUT_DETECTION &&
185+
process.env.INSTANA_ENABLE_LAMBDA_TIMEOUT_DETECTION === 'true'
186+
) {
187+
logger.debug('Heuristical timeout detection enabled. Please only use for debugging purposes.');
188+
registerTimeoutDetection(context, entrySpan);
189+
}
190+
178191
let handlerPromise;
179192
try {
180193
handlerPromise = originalHandler.apply(originalThis, originalArgs);
@@ -258,6 +271,50 @@ function init(event, arnInfo, _config) {
258271
tracing.activate();
259272
}
260273

274+
function registerTimeoutDetection(context, entrySpan) {
275+
// We register the timeout detection directly at the start so getRemainingTimeInMillis basically gives us the
276+
// configured timeout for this Lambda function, minus roughly 50 - 100 ms that is spent in bootstrapping.
277+
const initialRemainingMillis = getRemainingTimeInMillis(context);
278+
if (typeof initialRemainingMillis !== 'number') {
279+
return;
280+
}
281+
if (initialRemainingMillis <= 2500) {
282+
logger.debug(
283+
'Heuristical timeout detection will be disabled for Lambda functions with a short timeout ' +
284+
'(2 seconds and smaller).'
285+
);
286+
return;
287+
}
288+
289+
let triggerTimeoutHandlingAfter;
290+
if (initialRemainingMillis <= 4000) {
291+
// For Lambdas configured with a timeout of 3 or 4 seconds we heuristically assume a timeout when only
292+
// 10% of time is remaining.
293+
triggerTimeoutHandlingAfter = initialRemainingMillis * 0.9;
294+
} else {
295+
// For Lambdas configured with a timeout of 5 seconds or more we heuristically assume a timeout when only 400 ms of
296+
// time are remaining.
297+
triggerTimeoutHandlingAfter = initialRemainingMillis - 400;
298+
}
299+
300+
logger.debug(
301+
`Registering heuristical timeout detection to be triggered in ${triggerTimeoutHandlingAfter} milliseconds.`
302+
);
303+
304+
setTimeout(() => {
305+
postHandlerForTimeout(entrySpan, getRemainingTimeInMillis(context));
306+
}, triggerTimeoutHandlingAfter).unref();
307+
}
308+
309+
function getRemainingTimeInMillis(context) {
310+
if (context && typeof context.getRemainingTimeInMillis === 'function') {
311+
return context.getRemainingTimeInMillis();
312+
} else {
313+
logger.warn('context.getRemainingTimeInMillis() is not available, timeout detection will be disabled.');
314+
return null;
315+
}
316+
}
317+
261318
function shouldUseLambdaExtension() {
262319
if (process.env.INSTANA_DISABLE_LAMBDA_EXTENSION) {
263320
logger.info('INSTANA_DISABLE_LAMBDA_EXTENSION is set, not using the Lambda extension.');
@@ -390,6 +447,48 @@ function postHandler(entrySpan, error, result, callback) {
390447
});
391448
}
392449

450+
/**
451+
* When the timeout heuristic detects an imminent timeout, we finish the entry span prematurely and send it to the
452+
* back end.
453+
*/
454+
function postHandlerForTimeout(entrySpan, remainingMillis) {
455+
/**
456+
* context.getRemainingTimeInMillis(context) can return negative values
457+
* That just means that the lambda was already closed.
458+
* `setTimeout` is not 100% reliable
459+
*/
460+
if (remainingMillis < 200) {
461+
logger.debug('Skipping heuristical timeout detection because lambda timeout exceeded already.');
462+
return;
463+
}
464+
465+
if (entrySpan) {
466+
// CASE: Timeout not needed, we already send the data to the backend successfully
467+
if (entrySpan.transmitted) {
468+
logger.debug('Skipping heuristical timeout detection because BE data was sent already.');
469+
return;
470+
}
471+
472+
entrySpan.ec = 1;
473+
entrySpan.data.lambda.msleft = remainingMillis;
474+
entrySpan.data.lambda.error = `Possible Lambda timeout with only ${remainingMillis} ms left.`;
475+
entrySpan.d = Date.now() - entrySpan.ts;
476+
entrySpan.transmit();
477+
}
478+
479+
logger.debug(`Heuristical timeout detection was triggered with ${remainingMillis} milliseconds left.`);
480+
481+
// deliberately not gathering metrics but only sending spans.
482+
const spans = spanBuffer.getAndResetSpans();
483+
484+
sendToBackend({
485+
spans,
486+
metricsPayload: {},
487+
finalLambdaRequest: true,
488+
callback: () => {}
489+
});
490+
}
491+
393492
exports.currentSpan = function getHandleForCurrentSpan() {
394493
return tracing.getHandleForCurrentSpan();
395494
};

packages/aws-lambda/test/Control.js

+3
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ Control.prototype.reset = function reset() {
4545
};
4646

4747
Control.prototype.startMonitoredProcess = function startMonitoredProcess() {
48+
const self = this;
4849
const envs = {
4950
HANDLER_DEFINITION_PATH: this.opts.handlerDefinitionPath,
5051
DOWNSTREAM_DUMMY_URL: this.downstreamDummyUrl,
@@ -79,6 +80,8 @@ Control.prototype.startMonitoredProcess = function startMonitoredProcess() {
7980
}
8081
} else if (message.type === 'lambda-context') {
8182
this.clientContext = message.context.clientContext;
83+
} else if (message.type === 'lambda-timeout') {
84+
self.faasRuntime.kill();
8285
} else {
8386
this.messagesFromFaasRuntime.push(message);
8487
}

packages/aws-lambda/test/runtime_mock/index.js

+19
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,22 @@ function runHandler(handler, error, { context, event, eventOpts } = {}) {
129129
context = context || createContext(callback);
130130
addDirectInvokeTracingCorrelationToContext(context, trigger);
131131

132+
if (
133+
process.env.INSTANA_ENABLE_RUNTIME_TIMEOUT_DETECTION &&
134+
process.env.INSTANA_ENABLE_RUNTIME_TIMEOUT_DETECTION === 'true'
135+
) {
136+
setTimeout(() => {
137+
log('Lambda timed out.');
138+
139+
sendToParent('runtime: terminating');
140+
141+
sendToParent({
142+
type: 'lambda-timeout',
143+
error: true
144+
});
145+
}, context.getRemainingTimeInMillis());
146+
}
147+
132148
const promise = handler(event, context, callback);
133149

134150
if (promise && typeof promise.then === 'function') {
@@ -188,6 +204,9 @@ function createContext(callback) {
188204

189205
const configuredTimeout = process.env.LAMBDA_TIMEOUT ? parseInt(process.env.LAMBDA_TIMEOUT, 10) : 3000;
190206

207+
// eslint-disable-next-line no-console
208+
console.log(`Lambda timeout is set to ${configuredTimeout} ms.`);
209+
191210
const done = (err, result) => {
192211
callback(err, result);
193212
};
+28
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/*
2+
* (c) Copyright IBM Corp. 2021
3+
* (c) Copyright Instana Inc. and contributors 2020
4+
*/
5+
6+
/* eslint-disable no-console */
7+
8+
'use strict';
9+
10+
const instana = require('../..');
11+
12+
const delay = require('../../../core/test/test_util/delay');
13+
14+
if (!process.env.DELAY) {
15+
throw new Error('This Lambda requires the environment variable DELAY to be set.');
16+
}
17+
18+
exports.handler = instana.wrap(async () => {
19+
console.log(`Lambda handler started, now waiting ${process.env.DELAY} milliseconds.`);
20+
await delay(process.env.DELAY);
21+
console.log('Lambda handler finished');
22+
23+
return {
24+
body: {
25+
message: 'Stan says hi!'
26+
}
27+
};
28+
});

0 commit comments

Comments
 (0)