Skip to content

Commit 7eb6fef

Browse files
logaretmclaude
andcommitted
fix(aws-serverless): Add timeout to _endSpan forceFlush to prevent Lambda hanging
The vendored AwsLambdaInstrumentation._endSpan calls tracerProvider.forceFlush() without any timeout. Because _endSpan blocks the promise chain before wrapHandler's flush(2000), a hung forceFlush() prevents the Lambda from ever returning — causing it to sit idle until the runtime kills it at its configured timeout. Wrap the flush in a Promise.race with a 2s timeout to match wrapHandler's flush timeout, ensuring the callback always fires within a bounded time. Co-Authored-By: Claude <noreply@anthropic.com> Made-with: Cursor
1 parent 73f03bb commit 7eb6fef

File tree

2 files changed

+104
-1
lines changed

2 files changed

+104
-1
lines changed

packages/aws-serverless/src/integration/instrumentation-aws-lambda/instrumentation.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -504,7 +504,11 @@ export class AwsLambdaInstrumentation extends InstrumentationBase<AwsLambdaInstr
504504
);
505505
}
506506

507-
Promise.all(flushers).then(callback, callback);
507+
const FORCE_FLUSH_TIMEOUT_MS = 2000;
508+
Promise.race([
509+
Promise.all(flushers),
510+
new Promise<void>(resolve => setTimeout(resolve, FORCE_FLUSH_TIMEOUT_MS)),
511+
]).then(callback, callback);
508512
}
509513

510514
/**
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import { SpanStatusCode } from '@opentelemetry/api';
2+
import { afterEach, describe, expect, test, vi } from 'vitest';
3+
import { AwsLambdaInstrumentation } from '../src/integration/instrumentation-aws-lambda/instrumentation';
4+
5+
function createMockTracerProvider(forceFlushImpl: () => Promise<void>) {
6+
return {
7+
getTracer: () => ({
8+
startSpan: vi.fn(),
9+
startActiveSpan: vi.fn(),
10+
}),
11+
forceFlush: forceFlushImpl,
12+
};
13+
}
14+
15+
describe('AwsLambdaInstrumentation', () => {
16+
describe('_endSpan', () => {
17+
afterEach(() => {
18+
vi.useRealTimers();
19+
});
20+
21+
test('callback fires even when tracerProvider.forceFlush() never resolves', async () => {
22+
vi.useFakeTimers();
23+
24+
const instrumentation = new AwsLambdaInstrumentation();
25+
26+
const hangingProvider = createMockTracerProvider(() => new Promise<void>(() => {}));
27+
instrumentation.setTracerProvider(hangingProvider as any);
28+
29+
const mockSpan = {
30+
end: vi.fn(),
31+
recordException: vi.fn(),
32+
setStatus: vi.fn(),
33+
};
34+
35+
const callback = vi.fn();
36+
37+
(instrumentation as any)._endSpan(mockSpan, undefined, callback);
38+
39+
// Advance past any reasonable timeout (e.g. 5s) — the callback should fire
40+
// within a bounded time even if forceFlush() hangs forever.
41+
await vi.advanceTimersByTimeAsync(5_000);
42+
43+
expect(mockSpan.end).toHaveBeenCalled();
44+
expect(callback).toHaveBeenCalledTimes(1);
45+
46+
vi.useRealTimers();
47+
});
48+
49+
test('callback fires promptly when tracerProvider.forceFlush() resolves', async () => {
50+
const instrumentation = new AwsLambdaInstrumentation();
51+
52+
const normalProvider = createMockTracerProvider(() => Promise.resolve());
53+
instrumentation.setTracerProvider(normalProvider as any);
54+
55+
const mockSpan = {
56+
end: vi.fn(),
57+
recordException: vi.fn(),
58+
setStatus: vi.fn(),
59+
};
60+
61+
const callback = vi.fn();
62+
63+
(instrumentation as any)._endSpan(mockSpan, undefined, callback);
64+
65+
await new Promise(resolve => setTimeout(resolve, 10));
66+
67+
expect(callback).toHaveBeenCalledTimes(1);
68+
expect(mockSpan.end).toHaveBeenCalled();
69+
});
70+
71+
test('error information is set on span before flush attempt', async () => {
72+
const instrumentation = new AwsLambdaInstrumentation();
73+
74+
const normalProvider = createMockTracerProvider(() => Promise.resolve());
75+
instrumentation.setTracerProvider(normalProvider as any);
76+
77+
const mockSpan = {
78+
end: vi.fn(),
79+
recordException: vi.fn(),
80+
setStatus: vi.fn(),
81+
};
82+
83+
const error = new Error('lambda failure');
84+
const callback = vi.fn();
85+
86+
(instrumentation as any)._endSpan(mockSpan, error, callback);
87+
88+
await new Promise(resolve => setTimeout(resolve, 10));
89+
90+
expect(mockSpan.recordException).toHaveBeenCalledWith(error);
91+
expect(mockSpan.setStatus).toHaveBeenCalledWith({
92+
code: SpanStatusCode.ERROR,
93+
message: 'lambda failure',
94+
});
95+
expect(mockSpan.end).toHaveBeenCalled();
96+
expect(callback).toHaveBeenCalledTimes(1);
97+
});
98+
});
99+
});

0 commit comments

Comments
 (0)