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))
}