diff --git a/pageserver/src/http/routes.rs b/pageserver/src/http/routes.rs
index 4f4c397abe..1c5eacd362 100644
--- a/pageserver/src/http/routes.rs
+++ b/pageserver/src/http/routes.rs
@@ -738,17 +738,17 @@ async fn timeline_compact_handler(request: Request
) -> Result = result_receiver
+ .await
+ .context("receive compaction result")
+ .map_err(ApiError::InternalServerError)?;
+ result.map_err(ApiError::InternalServerError)?;
+
json_response(StatusCode::OK, ())
}
diff --git a/pageserver/src/tenant/mgr.rs b/pageserver/src/tenant/mgr.rs
index af7794490a..dce7cd8bae 100644
--- a/pageserver/src/tenant/mgr.rs
+++ b/pageserver/src/tenant/mgr.rs
@@ -492,3 +492,53 @@ pub async fn immediate_gc(
Ok(wait_task_done)
}
+
+#[cfg(feature = "testing")]
+pub async fn immediate_compact(
+ tenant_id: TenantId,
+ timeline_id: TimelineId,
+) -> Result>, ApiError> {
+ let guard = TENANTS.read().await;
+
+ let tenant = guard
+ .get(&tenant_id)
+ .map(Arc::clone)
+ .with_context(|| format!("Tenant {tenant_id} not found"))
+ .map_err(ApiError::NotFound)?;
+
+ let timeline = tenant
+ .get_timeline(timeline_id, true)
+ .map_err(ApiError::NotFound)?;
+
+ // Run in task_mgr to avoid race with detach operation
+ let (task_done, wait_task_done) = tokio::sync::oneshot::channel();
+ task_mgr::spawn(
+ &tokio::runtime::Handle::current(),
+ TaskKind::Compaction,
+ Some(tenant_id),
+ Some(timeline_id),
+ &format!(
+ "timeline_compact_handler compaction run for tenant {tenant_id} timeline {timeline_id}"
+ ),
+ false,
+ async move {
+ let result = timeline
+ .compact()
+ .instrument(
+ info_span!("manual_compact", tenant = %tenant_id, timeline = %timeline_id),
+ )
+ .await;
+
+ match task_done.send(result) {
+ Ok(_) => (),
+ Err(result) => error!("failed to send compaction result: {result:?}"),
+ }
+ Ok(())
+ },
+ );
+
+ // drop the guard until after we've spawned the task so that timeline shutdown will wait for the task
+ drop(guard);
+
+ Ok(wait_task_done)
+}