@@ -19,6 +19,7 @@ const runtimeApiClient = vi.hoisted(() => ({
1919 close : vi . fn ( ) ,
2020} ) )
2121const waitForMultipartPprofWindowMock = vi . hoisted ( ( ) => vi . fn ( ) )
22+ const getManagementMock = vi . hoisted ( ( ) => vi . fn ( ) )
2223
2324vi . mock ( '@platformatic/control' , ( ) => ( {
2425 RuntimeApiClient : class MockRuntimeApiClient {
@@ -31,6 +32,7 @@ vi.mock('@platformatic/control', () => ({
3132
3233vi . mock ( '@platformatic/globals' , ( ) => ( {
3334 getGlobal : vi . fn ( ) ,
35+ getManagement : getManagementMock ,
3436} ) )
3537
3638vi . mock ( '../../plugins/apikey' , ( ) => ( {
@@ -296,6 +298,7 @@ describe('admin pprof routes', () => {
296298 applicationId : 'storage' ,
297299 workerId : 2 ,
298300 } as never )
301+ getManagementMock . mockReturnValue ( undefined )
299302
300303 runtimeApiClient . getRuntimeApplications . mockResolvedValue ( {
301304 applications : [ { id : 'storage' , workers : 2 } ] ,
@@ -621,6 +624,80 @@ describe('admin pprof routes', () => {
621624 }
622625 } )
623626
627+ it ( 'retries whole-app captures with live worker ids from worker-not-found errors' , async ( ) => {
628+ runtimeApiClient . startApplicationProfiling . mockImplementation (
629+ async ( _pid , applicationId : string ) => {
630+ if ( applicationId === 'storage:0' || applicationId === 'storage:1' ) {
631+ throw Object . assign (
632+ new Error (
633+ `Failed to start profiling for service "${ applicationId } ": ` +
634+ 'Worker 0 of application storage not found. Available applications are: 4, 5, 6'
635+ ) ,
636+ {
637+ code : 'PLT_CTR_FAILED_TO_START_PROFILING' ,
638+ }
639+ )
640+ }
641+ }
642+ )
643+ runtimeApiClient . stopApplicationProfiling
644+ . mockResolvedValueOnce ( buildProfile ( 'worker-four' , 44 ) . buffer )
645+ . mockResolvedValueOnce ( buildProfile ( 'worker-five' , 55 ) . buffer )
646+ . mockResolvedValueOnce ( buildProfile ( 'worker-six' , 66 ) . buffer )
647+
648+ const app = await buildApp ( )
649+
650+ try {
651+ const response = await app . inject ( {
652+ method : 'GET' ,
653+ url : '/heap?seconds=1' ,
654+ } )
655+
656+ expect ( response . statusCode ) . toBe ( 200 )
657+ expect ( response . headers [ 'x-platformatic-worker-count' ] ) . toBe ( '3' )
658+ expect ( runtimeApiClient . startApplicationProfiling ) . toHaveBeenCalledTimes ( 5 )
659+ for ( const workerId of [ 0 , 1 , 4 , 5 , 6 ] ) {
660+ expect ( runtimeApiClient . startApplicationProfiling ) . toHaveBeenCalledWith (
661+ process . pid ,
662+ `storage:${ workerId } ` ,
663+ {
664+ type : 'heap' ,
665+ }
666+ )
667+ }
668+ expect ( runtimeApiClient . stopApplicationProfiling ) . toHaveBeenCalledTimes ( 3 )
669+ for ( const [ call , workerId ] of [
670+ [ 1 , 4 ] ,
671+ [ 2 , 5 ] ,
672+ [ 3 , 6 ] ,
673+ ] ) {
674+ expect ( runtimeApiClient . stopApplicationProfiling ) . toHaveBeenNthCalledWith (
675+ call ,
676+ process . pid ,
677+ `storage:${ workerId } ` ,
678+ {
679+ type : 'heap' ,
680+ }
681+ )
682+ }
683+
684+ const parts = parseMultipartParts ( response . rawPayload , response . headers [ 'content-type' ] )
685+ expect ( JSON . parse ( parts [ 0 ] . body . toString ( 'utf8' ) ) ) . toMatchObject ( {
686+ event : 'started' ,
687+ workerCount : 3 ,
688+ } )
689+
690+ const mergedProfile = Profile . decode ( parts [ 1 ] . body )
691+ expect (
692+ mergedProfile . sample
693+ . map ( ( sample ) => sample . value [ 0 ] )
694+ . sort ( ( left , right ) => Number ( left ) - Number ( right ) )
695+ ) . toEqual ( [ 44 , 55 , 66 ] )
696+ } finally {
697+ await app . close ( )
698+ }
699+ } )
700+
624701 it ( 'rolls back already-started workers when a whole-app start partially fails' , async ( ) => {
625702 runtimeApiClient . startApplicationProfiling
626703 . mockResolvedValueOnce ( undefined )
0 commit comments