@@ -175,6 +175,19 @@ function shimmedHandler(originalHandler, originalThis, originalArgs, _config) {
175
175
} ) ;
176
176
} ;
177
177
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
+
178
191
let handlerPromise ;
179
192
try {
180
193
handlerPromise = originalHandler . apply ( originalThis , originalArgs ) ;
@@ -258,6 +271,50 @@ function init(event, arnInfo, _config) {
258
271
tracing . activate ( ) ;
259
272
}
260
273
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
+
261
318
function shouldUseLambdaExtension ( ) {
262
319
if ( process . env . INSTANA_DISABLE_LAMBDA_EXTENSION ) {
263
320
logger . info ( 'INSTANA_DISABLE_LAMBDA_EXTENSION is set, not using the Lambda extension.' ) ;
@@ -390,6 +447,48 @@ function postHandler(entrySpan, error, result, callback) {
390
447
} ) ;
391
448
}
392
449
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
+
393
492
exports . currentSpan = function getHandleForCurrentSpan ( ) {
394
493
return tracing . getHandleForCurrentSpan ( ) ;
395
494
} ;
0 commit comments