diff --git a/pageserver/src/http/openapi_spec.yml b/pageserver/src/http/openapi_spec.yml index 46305a4855..ed190db43a 100644 --- a/pageserver/src/http/openapi_spec.yml +++ b/pageserver/src/http/openapi_spec.yml @@ -587,6 +587,8 @@ components: type: string state: type: string + current_physical_size: + type: integer has_in_progress_downloads: type: boolean TenantCreateInfo: diff --git a/pageserver/src/http/routes.rs b/pageserver/src/http/routes.rs index 8ac3faca7a..1582e8a2a4 100644 --- a/pageserver/src/http/routes.rs +++ b/pageserver/src/http/routes.rs @@ -438,14 +438,37 @@ async fn tenant_status(request: Request) -> Result, ApiErro let index_accessor = remote_index.read().await; let has_in_progress_downloads = index_accessor .tenant_entry(&tenant_id) - .ok_or_else(|| ApiError::NotFound("Tenant not found in remote index".to_string()))? - .has_in_progress_downloads(); + .map(|t| t.has_in_progress_downloads()) + .unwrap_or_else(|| { + info!("Tenant {tenant_id} not found in remote index"); + false + }); + + let current_physical_size = match tokio::task::spawn_blocking(move || { + crate::timelines::get_local_timelines(tenant_id, false, false) + }) + .await + .map_err(ApiError::from_err)? + { + Err(err) => { + // Getting local timelines can fail when no local repo is on disk (e.g, when tenant data is being downloaded). + // In that case, put a warning message into log and operate normally. + warn!("Failed to get local timelines for tenant {tenant_id}: {err}"); + None + } + Ok(local_timeline_infos) => Some( + local_timeline_infos + .into_iter() + .fold(0, |acc, x| acc + x.1.current_physical_size.unwrap()), + ), + }; json_response( StatusCode::OK, TenantInfo { id: tenant_id, state: tenant_state, + current_physical_size, has_in_progress_downloads: Some(has_in_progress_downloads), }, ) diff --git a/pageserver/src/tenant_mgr.rs b/pageserver/src/tenant_mgr.rs index 640dfa623a..3f88ab1be2 100644 --- a/pageserver/src/tenant_mgr.rs +++ b/pageserver/src/tenant_mgr.rs @@ -508,6 +508,7 @@ pub struct TenantInfo { #[serde_as(as = "DisplayFromStr")] pub id: ZTenantId, pub state: Option, + pub current_physical_size: Option, // physical size is only included in `tenant_status` endpoint pub has_in_progress_downloads: Option, } @@ -526,6 +527,7 @@ pub fn list_tenants(remote_index: &RemoteTimelineIndex) -> Vec { TenantInfo { id: *id, state: Some(tenant.state), + current_physical_size: None, has_in_progress_downloads, } }) diff --git a/test_runner/batch_others/test_timeline_size.py b/test_runner/batch_others/test_timeline_size.py index c3788a0e9b..f76529f1f7 100644 --- a/test_runner/batch_others/test_timeline_size.py +++ b/test_runner/batch_others/test_timeline_size.py @@ -1,5 +1,5 @@ from contextlib import closing -import pathlib +import random from uuid import UUID import re import psycopg2.extras @@ -298,6 +298,40 @@ def test_timeline_physical_size_metric(neon_simple_env: NeonEnv): assert tl_physical_size_metric == get_timeline_dir_size(timeline_path) +def test_tenant_physical_size(neon_simple_env: NeonEnv): + random.seed(100) + + env = neon_simple_env + client = env.pageserver.http_client() + + tenant, timeline = env.neon_cli.create_tenant() + + def get_timeline_physical_size(timeline: UUID): + res = client.timeline_detail(tenant, timeline) + return res['local']['current_physical_size_non_incremental'] + + timeline_total_size = get_timeline_physical_size(timeline) + for i in range(10): + n_rows = random.randint(100, 1000) + + timeline = env.neon_cli.create_branch(f"test_tenant_physical_size_{i}", tenant_id=tenant) + pg = env.postgres.create_start(f"test_tenant_physical_size_{i}", tenant_id=tenant) + + pg.safe_psql_many([ + "CREATE TABLE foo (t text)", + f"INSERT INTO foo SELECT 'long string to consume some space' || g FROM generate_series(1, {n_rows}) g", + ]) + + env.pageserver.safe_psql(f"checkpoint {tenant.hex} {timeline.hex}") + + timeline_total_size += get_timeline_physical_size(timeline) + + pg.stop() + + tenant_physical_size = int(client.tenant_status(tenant_id=tenant)['current_physical_size']) + assert tenant_physical_size == timeline_total_size + + def assert_physical_size(env: NeonEnv, tenant_id: UUID, timeline_id: UUID): """Check the current physical size returned from timeline API matches the total physical size of the timeline on disk"""