|
1 | 1 | 'use strict' |
2 | 2 |
|
3 | 3 | const assert = require('node:assert/strict') |
| 4 | +const { EventEmitter } = require('node:events') |
4 | 5 | const path = require('node:path') |
5 | 6 |
|
6 | 7 | const { afterEach, beforeEach, describe, it } = require('mocha') |
@@ -401,6 +402,97 @@ describe('lambda', () => { |
401 | 402 | } |
402 | 403 | }) |
403 | 404 | } |
| 405 | + |
| 406 | + for (const outcome of ['undefined', 'null', 'false', 'zero', 'object', 'artifact', |
| 407 | + 'fulfill', 'reject', 'throw', 'timeout']) { |
| 408 | + it(`handles ${outcome} streaming returns through the unmarked ${file} adapter`, async () => { |
| 409 | + process.env.LAMBDA_TASK_ROOT = '/var/task' |
| 410 | + process.env.DD_LAMBDA_HANDLER = 'handler.myEntry' |
| 411 | + const { hookCalls } = loadLambdaWithHookSpy() |
| 412 | + const fixturePath = `${path.resolve('/var/task', '', 'handler')}.js` |
| 413 | + const clock = sinon.useFakeTimers({ toFake: ['setTimeout', 'clearTimeout'] }) |
| 414 | + const oldTracer = global._ddtrace |
| 415 | + const beforeExit = Symbol.for('aws.lambda.beforeExit') |
| 416 | + const oldBeforeExit = globalThis[beforeExit] |
| 417 | + const killAll = sinon.spy() |
| 418 | + const span = { addTags: sinon.spy(), finish: sinon.spy() } |
| 419 | + const warmSpan = { addTags: sinon.spy(), finish: sinon.spy() } |
| 420 | + let activeSpan = span |
| 421 | + global._ddtrace = { |
| 422 | + _tracer: { |
| 423 | + _config: { DD_APM_FLUSH_DEADLINE_MILLISECONDS: 25 }, |
| 424 | + _processor: { killAll }, |
| 425 | + scope: () => ({ active: () => activeSpan }), |
| 426 | + }, |
| 427 | + } |
| 428 | + globalThis[beforeExit] = () => {} |
| 429 | + |
| 430 | + try { |
| 431 | + const stream = {} |
| 432 | + const context = { getRemainingTimeInMillis: () => 100 } |
| 433 | + const error = new Error('streaming handler failure') |
| 434 | + const values = { undefined, null: null, false: false, zero: 0, object: {}, artifact: new EventEmitter() } |
| 435 | + let complete = _result => assert.fail('The streaming handler did not capture its completion') |
| 436 | + const streaming = Symbol.for('aws.lambda.runtime.handler.streaming') |
| 437 | + const raw = (event, responseStream, ctx) => { |
| 438 | + assert.strictEqual(responseStream, stream) |
| 439 | + assert.strictEqual(ctx, context) |
| 440 | + if (event.pending || outcome === 'timeout') return new Promise(resolve => { complete = resolve }) |
| 441 | + if (outcome === 'throw') throw error |
| 442 | + if (outcome === 'fulfill') return Promise.resolve('done') |
| 443 | + if (outcome === 'reject') return Promise.reject(error) |
| 444 | + return values[outcome] |
| 445 | + } |
| 446 | + raw[streaming] = 'response' |
| 447 | + const app = hookCalls[0].onrequire({ myEntry: raw }, fixturePath, undefined, '0.0.0') |
| 448 | + const moduleExports = { |
| 449 | + promisifiedHandler: handler => { |
| 450 | + assert.strictEqual(handler[streaming], 'response') |
| 451 | + // datadog-lambda-js's streaming adapter forwards all three arguments but omits the marker. |
| 452 | + return (event, responseStream, context) => handler(event, responseStream, context) |
| 453 | + }, |
| 454 | + } |
| 455 | + hookCalls[1].onrequire(moduleExports, `datadog-lambda-js/${file}`, undefined, '12.143.0') |
| 456 | + const handler = moduleExports.promisifiedHandler(app.myEntry) |
| 457 | + assert.strictEqual(handler[streaming], undefined) |
| 458 | + const invoke = () => handler({}, stream, context) |
| 459 | + if (outcome === 'throw') assert.throws(invoke, candidate => candidate === error) |
| 460 | + else if (outcome === 'reject') await assert.rejects(invoke(), candidate => candidate === error) |
| 461 | + else if (outcome === 'timeout') { |
| 462 | + const invocation = invoke() |
| 463 | + clock.tick(74) |
| 464 | + sinon.assert.notCalled(killAll) |
| 465 | + clock.tick(1) |
| 466 | + sinon.assert.calledOnce(killAll) |
| 467 | + assert.strictEqual(span.addTags.firstCall.args[0]['error.type'], 'Impending Timeout') |
| 468 | + complete('done') |
| 469 | + assert.strictEqual(await invocation, 'done') |
| 470 | + } else assert.strictEqual(await invoke(), outcome === 'fulfill' ? 'done' : values[outcome]) |
| 471 | + assert.strictEqual(clock.countTimers(), 0, 'the first invocation must release its monitor') |
| 472 | + |
| 473 | + // RIC replaces its before-exit listener between invocations, so cleanup cannot rely on the old listener. |
| 474 | + globalThis[beforeExit] = () => {} |
| 475 | + clock.tick(25) |
| 476 | + activeSpan = warmSpan |
| 477 | + const warmInvocation = handler({ pending: true }, stream, context) |
| 478 | + clock.tick(50) |
| 479 | + assert.strictEqual(killAll.callCount, outcome === 'timeout' ? 1 : 0) |
| 480 | + sinon.assert.notCalled(warmSpan.addTags) |
| 481 | + sinon.assert.notCalled(warmSpan.finish) |
| 482 | + assert.strictEqual(clock.countTimers(), 1, 'the warm invocation must retain its own monitor') |
| 483 | + complete('warm result') |
| 484 | + assert.strictEqual(await warmInvocation, 'warm result') |
| 485 | + clock.tick(100) |
| 486 | + assert.strictEqual(killAll.callCount, outcome === 'timeout' ? 1 : 0) |
| 487 | + assert.strictEqual(clock.countTimers(), 0) |
| 488 | + } finally { |
| 489 | + clock.restore() |
| 490 | + global._ddtrace = oldTracer |
| 491 | + if (oldBeforeExit === undefined) delete globalThis[beforeExit] |
| 492 | + else globalThis[beforeExit] = oldBeforeExit |
| 493 | + } |
| 494 | + }) |
| 495 | + } |
404 | 496 | } |
405 | 497 |
|
406 | 498 | it('logs hook errors without unwinding the loop', () => { |
|
0 commit comments