@@ -604,12 +604,15 @@ async fn disable_profile_heap(
604604 Path ( profile) : Path < String > ,
605605) -> crate :: Result < impl axum:: response:: IntoResponse > {
606606 let profile_dir = heap_profile_path ( & profile) ?;
607- // Fail before disabling tracking so a wrong ID does not stop the live
608- // session and stream an empty archive.
607+ // Always stop tracking first: this must remain possible even if the
608+ // profile directory is gone or the ID was lost. `disable_tracking` is a
609+ // no-op when nothing is enabled, so a retry with the right ID streams the
610+ // already-finalized profile.
611+ tokio:: task:: spawn_blocking ( rheaper:: disable_tracking) . await ?;
612+ // Fail explicitly instead of streaming an empty archive for a wrong ID.
609613 tokio:: fs:: metadata ( & profile_dir) . await ?;
610614 let ( tx, rx) = tokio:: sync:: mpsc:: channel :: < bytes:: Bytes > ( 1 ) ;
611615 tokio:: task:: spawn_blocking ( move || {
612- rheaper:: disable_tracking ( ) ;
613616 let sink =
614617 PollSender :: new ( tx) . sink_map_err ( |_| std:: io:: Error :: from ( ErrorKind :: BrokenPipe ) ) ;
615618 let writer = tokio_util:: io:: SyncIoBridge :: new ( SinkWriter :: new ( CopyToBytes :: new ( sink) ) ) ;
@@ -734,4 +737,67 @@ mod tests {
734737
735738 assert_eq ! ( response. status( ) , StatusCode :: INTERNAL_SERVER_ERROR ) ;
736739 }
740+
741+ #[ tokio:: test]
742+ async fn disable_profile_heap_stops_live_session_and_streams_profile ( ) {
743+ // The tracker is process-global: this must stay the only test that
744+ // enables tracking.
745+ let profile_dir = rheaper:: enable_tracking ( rheaper:: TrackerConfig {
746+ max_stack_depth : 1 ,
747+ max_trackers : 1 ,
748+ tracker_event_buffer_size : 1 ,
749+ sample_rate : 0.0 ,
750+ profile_dir : PathBuf :: from ( HEAP_PROFILE_DIR ) ,
751+ } )
752+ . unwrap ( ) ;
753+ let profile = profile_dir
754+ . file_name ( )
755+ . unwrap ( )
756+ . to_str ( )
757+ . unwrap ( )
758+ . to_owned ( ) ;
759+
760+ let app = axum:: Router :: new ( ) . route (
761+ "/profile/heap/disable/:id" ,
762+ axum:: routing:: post ( disable_profile_heap) ,
763+ ) ;
764+ let response = app
765+ . oneshot (
766+ Request :: builder ( )
767+ . method ( Method :: POST )
768+ . uri ( format ! ( "/profile/heap/disable/{profile}" ) )
769+ . body ( Body :: empty ( ) )
770+ . unwrap ( ) ,
771+ )
772+ . await
773+ . unwrap ( ) ;
774+ let status = response. status ( ) ;
775+ let body = hyper:: body:: to_bytes ( response. into_body ( ) ) . await . unwrap ( ) ;
776+ std:: fs:: remove_dir_all ( & profile_dir) . unwrap ( ) ;
777+
778+ assert_eq ! ( status, StatusCode :: OK ) ;
779+ let mut archive = tar:: Archive :: new ( & body[ ..] ) ;
780+ let entries: Vec < String > = archive
781+ . entries ( )
782+ . unwrap ( )
783+ . map ( |e| e. unwrap ( ) . path ( ) . unwrap ( ) . to_string_lossy ( ) . into_owned ( ) )
784+ . collect ( ) ;
785+ assert ! (
786+ entries. iter( ) . any( |e| e. trim_end_matches( '/' ) == profile) ,
787+ "tar entries: {entries:?}"
788+ ) ;
789+ // Tracking is off again, so enabling must succeed and be cleaned up.
790+ let again = rheaper:: enable_tracking ( rheaper:: TrackerConfig {
791+ max_stack_depth : 1 ,
792+ max_trackers : 1 ,
793+ tracker_event_buffer_size : 1 ,
794+ sample_rate : 0.0 ,
795+ profile_dir : PathBuf :: from ( HEAP_PROFILE_DIR ) ,
796+ } )
797+ . unwrap ( ) ;
798+ rheaper:: disable_tracking ( ) ;
799+ std:: fs:: remove_dir_all ( & again) . unwrap ( ) ;
800+ // Only removes the parent if nothing else is in it.
801+ let _ = std:: fs:: remove_dir ( HEAP_PROFILE_DIR ) ;
802+ }
737803}
0 commit comments