From 34b6bd416a8df8cf0d51f707beaca30dbdbe2adc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Arpad=20M=C3=BCller?= Date: Mon, 21 Oct 2024 17:33:05 +0200 Subject: [PATCH] offloaded timeline list API (#9461) Add a way to list the offloaded timelines. Before, one had to look at logs to figure out if a timeline has been offloaded or not, or use the non-presence of a certain timeline in the list of normal timelines. Now, one can list them directly. Part of #8088 --- libs/pageserver_api/src/models.rs | 17 +++++++ pageserver/src/http/routes.rs | 80 ++++++++++++++++++++++++++++++- pageserver/src/tenant.rs | 25 +++++++++- 3 files changed, 120 insertions(+), 2 deletions(-) diff --git a/libs/pageserver_api/src/models.rs b/libs/pageserver_api/src/models.rs index 5b0b6bebe3..e08bf40801 100644 --- a/libs/pageserver_api/src/models.rs +++ b/libs/pageserver_api/src/models.rs @@ -684,6 +684,23 @@ pub struct TimelineArchivalConfigRequest { pub state: TimelineArchivalState, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct TimelinesInfoAndOffloaded { + pub timelines: Vec, + pub offloaded: Vec, +} + +/// Analog of [`TimelineInfo`] for offloaded timelines. +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct OffloadedTimelineInfo { + pub tenant_id: TenantShardId, + pub timeline_id: TimelineId, + /// Whether the timeline has a parent it has been branched off from or not + pub ancestor_timeline_id: Option, + /// Whether to retain the branch lsn at the ancestor or not + pub ancestor_retain_lsn: Option, +} + /// This represents the output of the "timeline_detail" and "timeline_list" API calls. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct TimelineInfo { diff --git a/pageserver/src/http/routes.rs b/pageserver/src/http/routes.rs index 8f928fd81b..a254f1683d 100644 --- a/pageserver/src/http/routes.rs +++ b/pageserver/src/http/routes.rs @@ -26,6 +26,7 @@ use pageserver_api::models::LocationConfigListResponse; use pageserver_api::models::LocationConfigMode; use pageserver_api::models::LsnLease; use pageserver_api::models::LsnLeaseRequest; +use pageserver_api::models::OffloadedTimelineInfo; use pageserver_api::models::ShardParameters; use pageserver_api::models::TenantDetails; use pageserver_api::models::TenantLocationConfigRequest; @@ -37,6 +38,7 @@ use pageserver_api::models::TenantShardSplitRequest; use pageserver_api::models::TenantShardSplitResponse; use pageserver_api::models::TenantSorting; use pageserver_api::models::TimelineArchivalConfigRequest; +use pageserver_api::models::TimelinesInfoAndOffloaded; use pageserver_api::models::TopTenantShardItem; use pageserver_api::models::TopTenantShardsRequest; use pageserver_api::models::TopTenantShardsResponse; @@ -81,6 +83,7 @@ use crate::tenant::timeline::CompactFlags; use crate::tenant::timeline::CompactionError; use crate::tenant::timeline::Timeline; use crate::tenant::GetTimelineError; +use crate::tenant::OffloadedTimeline; use crate::tenant::{LogicalSizeCalculationCause, PageReconstructError}; use crate::{disk_usage_eviction_task, tenant}; use pageserver_api::models::{ @@ -477,6 +480,22 @@ async fn build_timeline_info_common( Ok(info) } +fn build_timeline_offloaded_info(offloaded: &Arc) -> OffloadedTimelineInfo { + let &OffloadedTimeline { + tenant_shard_id, + timeline_id, + ancestor_retain_lsn, + ancestor_timeline_id, + .. + } = offloaded.as_ref(); + OffloadedTimelineInfo { + tenant_id: tenant_shard_id, + timeline_id, + ancestor_retain_lsn, + ancestor_timeline_id, + } +} + // healthcheck handler async fn status_handler( request: Request, @@ -643,7 +662,7 @@ async fn timeline_list_handler( ) .instrument(info_span!("build_timeline_info", timeline_id = %timeline.timeline_id)) .await - .context("Failed to convert tenant timeline {timeline_id} into the local one: {e:?}") + .context("Failed to build timeline info") .map_err(ApiError::InternalServerError)?; response_data.push(timeline_info); @@ -658,6 +677,62 @@ async fn timeline_list_handler( json_response(StatusCode::OK, response_data) } +async fn timeline_and_offloaded_list_handler( + request: Request, + _cancel: CancellationToken, +) -> Result, ApiError> { + let tenant_shard_id: TenantShardId = parse_request_param(&request, "tenant_shard_id")?; + let include_non_incremental_logical_size: Option = + parse_query_param(&request, "include-non-incremental-logical-size")?; + let force_await_initial_logical_size: Option = + parse_query_param(&request, "force-await-initial-logical-size")?; + check_permission(&request, Some(tenant_shard_id.tenant_id))?; + + let state = get_state(&request); + let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Download); + + let response_data = async { + let tenant = state + .tenant_manager + .get_attached_tenant_shard(tenant_shard_id)?; + + tenant.wait_to_become_active(ACTIVE_TENANT_TIMEOUT).await?; + + let (timelines, offloadeds) = tenant.list_timelines_and_offloaded(); + + let mut timeline_infos = Vec::with_capacity(timelines.len()); + for timeline in timelines { + let timeline_info = build_timeline_info( + &timeline, + include_non_incremental_logical_size.unwrap_or(false), + force_await_initial_logical_size.unwrap_or(false), + &ctx, + ) + .instrument(info_span!("build_timeline_info", timeline_id = %timeline.timeline_id)) + .await + .context("Failed to build timeline info") + .map_err(ApiError::InternalServerError)?; + + timeline_infos.push(timeline_info); + } + let offloaded_infos = offloadeds + .into_iter() + .map(|offloaded| build_timeline_offloaded_info(&offloaded)) + .collect::>(); + let res = TimelinesInfoAndOffloaded { + timelines: timeline_infos, + offloaded: offloaded_infos, + }; + Ok::(res) + } + .instrument(info_span!("timeline_and_offloaded_list", + tenant_id = %tenant_shard_id.tenant_id, + shard_id = %tenant_shard_id.shard_slug())) + .await?; + + json_response(StatusCode::OK, response_data) +} + async fn timeline_preserve_initdb_handler( request: Request, _cancel: CancellationToken, @@ -2993,6 +3068,9 @@ pub fn make_router( .get("/v1/tenant/:tenant_shard_id/timeline", |r| { api_handler(r, timeline_list_handler) }) + .get("/v1/tenant/:tenant_shard_id/timeline_and_offloaded", |r| { + api_handler(r, timeline_and_offloaded_list_handler) + }) .post("/v1/tenant/:tenant_shard_id/timeline", |r| { api_handler(r, timeline_create_handler) }) diff --git a/pageserver/src/tenant.rs b/pageserver/src/tenant.rs index 1066d165cd..41d21ef041 100644 --- a/pageserver/src/tenant.rs +++ b/pageserver/src/tenant.rs @@ -1755,7 +1755,7 @@ impl Tenant { } /// Lists timelines the tenant contains. - /// Up to tenant's implementation to omit certain timelines that ar not considered ready for use. + /// It's up to callers to omit certain timelines that are not considered ready for use. pub fn list_timelines(&self) -> Vec> { self.timelines .lock() @@ -1765,6 +1765,29 @@ impl Tenant { .collect() } + /// Lists timelines the tenant manages, including offloaded ones. + /// + /// It's up to callers to omit certain timelines that are not considered ready for use. + pub fn list_timelines_and_offloaded( + &self, + ) -> (Vec>, Vec>) { + let timelines = self + .timelines + .lock() + .unwrap() + .values() + .map(Arc::clone) + .collect(); + let offloaded = self + .timelines_offloaded + .lock() + .unwrap() + .values() + .map(Arc::clone) + .collect(); + (timelines, offloaded) + } + pub fn list_timeline_ids(&self) -> Vec { self.timelines.lock().unwrap().keys().cloned().collect() }