Skip to content

Commit 1673711

Browse files
authored
fix(instrumentation-mongodb): Guard against multiple callback invocations (open-telemetry#3293)
1 parent 78df9f5 commit 1673711

File tree

2 files changed

+178
-13
lines changed

2 files changed

+178
-13
lines changed

packages/instrumentation-mongodb/src/instrumentation.ts

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,28 +1013,34 @@ export class MongoDBInstrumentation extends InstrumentationBase<MongoDBInstrumen
10131013
// in final callback (resultHandler) is lost
10141014
const activeContext = context.active();
10151015
const instrumentation = this;
1016+
let spanEnded = false;
1017+
10161018
return function patchedEnd(this: {}, ...args: unknown[]) {
1017-
const error = args[0];
1018-
if (span) {
1019-
if (error instanceof Error) {
1020-
span?.setStatus({
1021-
code: SpanStatusCode.ERROR,
1022-
message: error.message,
1023-
});
1024-
} else {
1025-
const result = args[1] as CommandResult;
1026-
instrumentation._handleExecutionResult(span, result);
1019+
if (!spanEnded) {
1020+
spanEnded = true;
1021+
const error = args[0];
1022+
if (span) {
1023+
if (error instanceof Error) {
1024+
span.setStatus({
1025+
code: SpanStatusCode.ERROR,
1026+
message: error.message,
1027+
});
1028+
} else {
1029+
const result = args[1] as CommandResult;
1030+
instrumentation._handleExecutionResult(span, result);
1031+
}
1032+
span.end();
10271033
}
1028-
span.end();
1029-
}
10301034

1031-
return context.with(activeContext, () => {
10321035
if (commandType === 'endSessions') {
10331036
instrumentation._connectionsUsage.add(-1, {
10341037
state: 'idle',
10351038
'pool.name': instrumentation._poolName,
10361039
});
10371040
}
1041+
}
1042+
1043+
return context.with(activeContext, () => {
10381044
return resultHandler.apply(this, args);
10391045
});
10401046
};

packages/instrumentation-mongodb/test/unit/instrumentation.test.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,3 +278,162 @@ describe('DbStatementSerializer', () => {
278278
});
279279
});
280280
});
281+
282+
describe('_patchEnd', () => {
283+
let instrumentation: MongoDBInstrumentation;
284+
285+
beforeEach(() => {
286+
instrumentation = new MongoDBInstrumentation();
287+
});
288+
289+
afterEach(() => {
290+
instrumentation.disable();
291+
});
292+
293+
function createMockSpan() {
294+
let endCallCount = 0;
295+
let setStatusCallCount = 0;
296+
let lastStatus: any = null;
297+
298+
return {
299+
end: () => {
300+
endCallCount++;
301+
},
302+
setStatus: (status: any) => {
303+
setStatusCallCount++;
304+
lastStatus = status;
305+
},
306+
recordException: () => {},
307+
setAttribute: () => {},
308+
setAttributes: () => {},
309+
addEvent: () => {},
310+
isRecording: () => endCallCount === 0,
311+
getEndCallCount: () => endCallCount,
312+
getSetStatusCallCount: () => setStatusCallCount,
313+
getLastStatus: () => lastStatus,
314+
};
315+
}
316+
317+
// https://github.com/open-telemetry/opentelemetry-js-contrib/issues/2788
318+
describe('double callback invocation guard', () => {
319+
it('should only call span.end() once even when callback is invoked multiple times', () => {
320+
const mockSpan = createMockSpan();
321+
let resultHandlerCallCount = 0;
322+
323+
const resultHandler = () => {
324+
resultHandlerCallCount++;
325+
return 'result';
326+
};
327+
328+
const patchedEnd = (instrumentation as any)._patchEnd(
329+
mockSpan,
330+
resultHandler,
331+
123,
332+
'find'
333+
);
334+
335+
patchedEnd(null, { ok: 1 });
336+
patchedEnd(null, { ok: 1 });
337+
patchedEnd(null, { ok: 1 });
338+
339+
assert.strictEqual(mockSpan.getEndCallCount(), 1);
340+
assert.strictEqual(resultHandlerCallCount, 3);
341+
});
342+
343+
it('should only set error status once when callback is invoked multiple times with error', () => {
344+
const mockSpan = createMockSpan();
345+
const resultHandler = () => 'result';
346+
347+
const patchedEnd = (instrumentation as any)._patchEnd(
348+
mockSpan,
349+
resultHandler,
350+
123,
351+
'find'
352+
);
353+
354+
const testError = new Error('Connection timeout');
355+
patchedEnd(testError);
356+
patchedEnd(testError);
357+
patchedEnd(testError);
358+
359+
assert.strictEqual(mockSpan.getSetStatusCallCount(), 1);
360+
assert.strictEqual(mockSpan.getEndCallCount(), 1);
361+
assert.strictEqual(mockSpan.getLastStatus().code, 2);
362+
assert.strictEqual(
363+
mockSpan.getLastStatus().message,
364+
'Connection timeout'
365+
);
366+
});
367+
368+
it('should handle undefined span gracefully on multiple invocations', () => {
369+
let resultHandlerCallCount = 0;
370+
const resultHandler = () => {
371+
resultHandlerCallCount++;
372+
return 'result';
373+
};
374+
375+
const patchedEnd = (instrumentation as any)._patchEnd(
376+
undefined,
377+
resultHandler,
378+
123,
379+
'find'
380+
);
381+
382+
assert.doesNotThrow(() => {
383+
patchedEnd(null, { ok: 1 });
384+
patchedEnd(null, { ok: 1 });
385+
patchedEnd(null, { ok: 1 });
386+
});
387+
assert.strictEqual(resultHandlerCallCount, 3);
388+
});
389+
390+
it('should only update metrics once for endSessions command', () => {
391+
const mockSpan = createMockSpan();
392+
let metricsAddCallCount = 0;
393+
394+
(instrumentation as any)._connectionsUsage = {
395+
add: () => {
396+
metricsAddCallCount++;
397+
},
398+
};
399+
(instrumentation as any)._poolName = 'mongodb://localhost:27017/test';
400+
401+
const patchedEnd = (instrumentation as any)._patchEnd(
402+
mockSpan,
403+
() => 'result',
404+
123,
405+
'endSessions'
406+
);
407+
408+
patchedEnd(null, { ok: 1 });
409+
patchedEnd(null, { ok: 1 });
410+
patchedEnd(null, { ok: 1 });
411+
412+
assert.strictEqual(metricsAddCallCount, 1);
413+
assert.strictEqual(mockSpan.getEndCallCount(), 1);
414+
});
415+
416+
it('should handle mixed success and error calls correctly', () => {
417+
const mockSpan = createMockSpan();
418+
let resultHandlerCallCount = 0;
419+
const resultHandler = () => {
420+
resultHandlerCallCount++;
421+
};
422+
423+
const patchedEnd = (instrumentation as any)._patchEnd(
424+
mockSpan,
425+
resultHandler,
426+
123,
427+
'find'
428+
);
429+
430+
patchedEnd(null, { ok: 1 });
431+
patchedEnd(new Error('Late error 1'));
432+
patchedEnd(new Error('Late error 2'));
433+
434+
assert.strictEqual(mockSpan.getEndCallCount(), 1);
435+
assert.strictEqual(mockSpan.getSetStatusCallCount(), 0);
436+
assert.strictEqual(resultHandlerCallCount, 3);
437+
});
438+
});
439+
});

0 commit comments

Comments
 (0)