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"""