diff --git a/pageserver/src/http/routes.rs b/pageserver/src/http/routes.rs index b8cb44c732..f5a9f2b16a 100644 --- a/pageserver/src/http/routes.rs +++ b/pageserver/src/http/routes.rs @@ -6,6 +6,7 @@ use std::sync::Arc; use anyhow::{anyhow, Context, Result}; use futures::TryFutureExt; +use hyper::header::CONTENT_TYPE; use hyper::StatusCode; use hyper::{Body, Request, Response, Uri}; use metrics::launch_timestamp::LaunchTimestamp; @@ -1236,6 +1237,136 @@ async fn deletion_queue_flush( } } +/// Try if `GetPage@Lsn` is successful, useful for manual debugging. +async fn getpage_at_lsn_handler( + request: Request, + _cancel: CancellationToken, +) -> 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))?; + + struct Key(crate::repository::Key); + + impl std::str::FromStr for Key { + type Err = anyhow::Error; + + fn from_str(s: &str) -> std::result::Result { + crate::repository::Key::from_hex(s).map(Key) + } + } + + let key: Key = parse_query_param(&request, "key")? + .ok_or_else(|| ApiError::BadRequest(anyhow!("missing 'key' query parameter")))?; + let lsn: Lsn = parse_query_param(&request, "lsn")? + .ok_or_else(|| ApiError::BadRequest(anyhow!("missing 'lsn' query parameter")))?; + + async { + let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Download); + let timeline = active_timeline_of_active_tenant(tenant_id, timeline_id).await?; + + let page = timeline.get(key.0, lsn, &ctx).await?; + + Result::<_, ApiError>::Ok( + Response::builder() + .status(StatusCode::OK) + .header(CONTENT_TYPE, "application/octet-stream") + .body(hyper::Body::from(page)) + .unwrap(), + ) + } + .instrument(info_span!("timeline_get", %tenant_id, %timeline_id)) + .await +} + +async fn timeline_collect_keyspace( + request: Request, + _cancel: CancellationToken, +) -> 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))?; + + struct Partitioning { + keys: crate::keyspace::KeySpace, + + at_lsn: Lsn, + } + + impl serde::Serialize for Partitioning { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeMap; + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_key("keys")?; + map.serialize_value(&KeySpace(&self.keys))?; + map.serialize_key("at_lsn")?; + map.serialize_value(&WithDisplay(&self.at_lsn))?; + map.end() + } + } + + struct WithDisplay<'a, T>(&'a T); + + impl<'a, T: std::fmt::Display> serde::Serialize for WithDisplay<'a, T> { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + serializer.collect_str(&self.0) + } + } + + struct KeySpace<'a>(&'a crate::keyspace::KeySpace); + + impl<'a> serde::Serialize for KeySpace<'a> { + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeSeq; + let mut seq = serializer.serialize_seq(Some(self.0.ranges.len()))?; + for kr in &self.0.ranges { + seq.serialize_element(&KeyRange(kr))?; + } + seq.end() + } + } + + struct KeyRange<'a>(&'a std::ops::Range); + + impl<'a> serde::Serialize for KeyRange<'a> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + use serde::ser::SerializeTuple; + let mut t = serializer.serialize_tuple(2)?; + t.serialize_element(&WithDisplay(&self.0.start))?; + t.serialize_element(&WithDisplay(&self.0.end))?; + t.end() + } + } + + let at_lsn: Option = parse_query_param(&request, "at_lsn")?; + + async { + let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Download); + let timeline = active_timeline_of_active_tenant(tenant_id, timeline_id).await?; + let at_lsn = at_lsn.unwrap_or_else(|| timeline.get_last_record_lsn()); + let keys = timeline + .collect_keyspace(at_lsn, &ctx) + .await + .map_err(ApiError::InternalServerError)?; + + json_response(StatusCode::OK, Partitioning { keys, at_lsn }) + } + .instrument(info_span!("timeline_collect_keyspace", %tenant_id, %timeline_id)) + .await +} + async fn active_timeline_of_active_tenant( tenant_id: TenantId, timeline_id: TimelineId, @@ -1583,5 +1714,12 @@ pub fn make_router( .post("/v1/tracing/event", |r| { testing_api_handler("emit a tracing event", r, post_tracing_event_handler) }) + .get("/v1/tenant/:tenant_id/timeline/:timeline_id/getpage", |r| { + testing_api_handler("getpage@lsn", r, getpage_at_lsn_handler) + }) + .get( + "/v1/tenant/:tenant_id/timeline/:timeline_id/keyspace", + |r| testing_api_handler("read out the keyspace", r, timeline_collect_keyspace), + ) .any(handler_404)) }