@@ -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