diff --git a/pageserver/src/http/routes.rs b/pageserver/src/http/routes.rs index e3b8b3305a..8685375b39 100644 --- a/pageserver/src/http/routes.rs +++ b/pageserver/src/http/routes.rs @@ -7,6 +7,7 @@ use pageserver_api::models::DownloadRemoteLayersTaskSpawnRequest; use remote_storage::GenericRemoteStorage; use tokio_util::sync::CancellationToken; use tracing::*; +use utils::http::request::get_request_param; use super::models::{ StatusResponse, TenantConfigRequest, TenantCreateRequest, TenantCreateResponse, TenantInfo, @@ -556,9 +557,7 @@ async fn tenant_size_handler(request: Request) -> Result, A ) } -async fn timeline_layer_map_info_handler( - request: Request, -) -> Result, ApiError> { +async fn layer_map_info_handler(request: Request) -> Result, ApiError> { let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?; let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?; check_permission(&request, Some(tenant_id))?; @@ -575,6 +574,58 @@ async fn timeline_layer_map_info_handler( json_response(StatusCode::OK, layer_map_info) } +async fn layer_download_handler(request: Request) -> Result, ApiError> { + let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?; + let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?; + let layer_file_name = get_request_param(&request, "layer_file_name")?; + check_permission(&request, Some(tenant_id))?; + + let tenant = mgr::get_tenant(tenant_id, true) + .await + .map_err(ApiError::NotFound)?; + let timeline = tenant + .get_timeline(timeline_id, true) + .map_err(ApiError::NotFound)?; + + let downloaded = timeline + .download_layer(layer_file_name) + .await + .map_err(ApiError::InternalServerError)?; + + let status = if downloaded { + StatusCode::OK + } else { + StatusCode::NOT_MODIFIED + }; + json_response(status, ()) +} + +async fn evict_timeline_layer_handler(request: Request) -> Result, ApiError> { + let tenant_id: TenantId = parse_request_param(&request, "tenant_id")?; + let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?; + let layer_file_name = get_request_param(&request, "layer_file_name")?; + check_permission(&request, Some(tenant_id))?; + + let tenant = mgr::get_tenant(tenant_id, true) + .await + .map_err(ApiError::NotFound)?; + let timeline = tenant + .get_timeline(timeline_id, true) + .map_err(ApiError::NotFound)?; + + let evicted = timeline + .evict_layer(layer_file_name) + .await + .map_err(ApiError::InternalServerError)?; + + let status = if evicted { + StatusCode::OK + } else { + StatusCode::NOT_MODIFIED + }; + json_response(status, ()) +} + // Helper function to standardize the error messages we produce on bad durations // // Intended to be used with anyhow's `with_context`, e.g.: @@ -1007,7 +1058,15 @@ pub fn make_router( ) .get( "/v1/tenant/:tenant_id/timeline/:timeline_id/layer", - timeline_layer_map_info_handler, + layer_map_info_handler, + ) + .get( + "/v1/tenant/:tenant_id/timeline/:timeline_id/layer/:layer_file_name", + layer_download_handler, + ) + .delete( + "/v1/tenant/:tenant_id/timeline/:timeline_id/layer/:layer_file_name", + evict_timeline_layer_handler, ) .any(handler_404)) } diff --git a/pageserver/src/tenant/timeline.rs b/pageserver/src/tenant/timeline.rs index 61697e1837..2e2d3e2e22 100644 --- a/pageserver/src/tenant/timeline.rs +++ b/pageserver/src/tenant/timeline.rs @@ -839,6 +839,73 @@ impl Timeline { historic_layers, } } + + pub async fn download_layer(&self, layer_file_name: &str) -> anyhow::Result { + if self.remote_client.is_none() { + return Ok(false); + } + let Some(remote_layer) = self.find_layer(layer_file_name) + .and_then(PersistentLayer::downcast_remote_layer) + else { + return Ok(false); + }; + + self.download_remote_layer(remote_layer) + .await + .map(|()| true) + } + + pub async fn evict_layer(&self, layer_file_name: &str) -> anyhow::Result { + let Some(remote_client) = &self.remote_client + else { + return Ok(false); + }; + let Some(local_layer) = self.find_layer(layer_file_name) + else { + return Ok(false); + }; + if local_layer.is_remote_layer() { + return Ok(false); + } + + // ensure the current layer is uploaded for sure + remote_client + .wait_completion() + .await + .context("wait for layer upload ops to complete")?; + + let layer_metadata = LayerFileMetadata::new( + local_layer + .file_size() + .expect("Local layer should have a file size"), + ); + let new_remote_layer = Arc::new(match local_layer.filename() { + LayerFileName::Image(image_name) => RemoteLayer::new_img( + self.tenant_id, + self.timeline_id, + &image_name, + &layer_metadata, + ), + LayerFileName::Delta(delta_name) => RemoteLayer::new_delta( + self.tenant_id, + self.timeline_id, + &delta_name, + &layer_metadata, + ), + #[cfg(test)] + LayerFileName::Test(_) => unreachable!(), + }); + + let mut layers = self.layers.write().unwrap(); + let mut updates = layers.batch_update(); + local_layer.delete()?; + updates.remove_historic(local_layer); + updates.insert_historic(new_remote_layer); + updates.flush(); + drop(layers); + + Ok(true) + } } // Private functions @@ -1628,6 +1695,17 @@ impl Timeline { Err(e) => error!("Failed to compute current logical size for metrics update: {e:?}"), } } + + fn find_layer(&self, layer_file_name: &str) -> Option> { + for historic_layer in self.layers.read().unwrap().iter_historic_layers() { + let historic_layer_name = historic_layer.filename().file_name(); + if layer_file_name == historic_layer_name { + return Some(historic_layer); + } + } + + None + } } type TraversalId = String;