Skip to content

Commit 9196686

Browse files
Stop heap tracking before checking the profile directory
1 parent 61e0984 commit 9196686

1 file changed

Lines changed: 69 additions & 3 deletions

File tree

  • libsql-server/src/http/admin

‎libsql-server/src/http/admin/mod.rs‎

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)