diff --git a/pageserver/src/http/routes.rs b/pageserver/src/http/routes.rs
index 2995a37089..8112831766 100644
--- a/pageserver/src/http/routes.rs
+++ b/pageserver/src/http/routes.rs
@@ -2181,8 +2181,29 @@ async fn handle_tenant_break(
json_response(StatusCode::OK, ())
}
+async fn get_lsn_leases_handler(
+ mut request: Request
,
+ _cancel: CancellationToken,
+) -> Result, ApiError> {
+ let tenant_shard_id: TenantShardId = parse_request_param(&request, "tenant_shard_id")?;
+ let timeline_id: TimelineId = parse_request_param(&request, "timeline_id")?;
+ check_permission(&request, Some(tenant_shard_id.tenant_id))?;
+
+ let ctx = RequestContext::new(TaskKind::MgmtRequest, DownloadBehavior::Download);
+
+ let state = get_state(&request);
+
+ let timeline =
+ active_timeline_of_active_tenant(&state.tenant_manager, tenant_shard_id, timeline_id)
+ .await?;
+
+ let leases = timeline.gc_info.read().unwrap().leases.clone();
+
+ json_response(StatusCode::OK, leases)
+}
+
// Obtains an lsn lease on the given timeline.
-async fn lsn_lease_handler(
+async fn post_lsn_lease_handler(
mut request: Request,
_cancel: CancellationToken,
) -> Result, ApiError> {
@@ -4002,9 +4023,14 @@ pub fn make_router(
"/v1/tenant/:tenant_shard_id/timeline/:timeline_id/patch_index_part",
|r| api_handler(r, timeline_patch_index_part_handler),
)
+ .get(
+ "/v1/tenant/:tenant_shard_id/timeline/:timeline_id/lsn_lease",
+
+ |r| api_handler(r, get_lsn_leases_handler)
+ )
.post(
"/v1/tenant/:tenant_shard_id/timeline/:timeline_id/lsn_lease",
- |r| api_handler(r, lsn_lease_handler),
+ |r| api_handler(r, post_lsn_lease_handler),
)
.put(
"/v1/tenant/:tenant_shard_id/timeline/:timeline_id/do_gc",
diff --git a/test_runner/fixtures/pageserver/http.py b/test_runner/fixtures/pageserver/http.py
index d9037f2d08..15c76594a9 100644
--- a/test_runner/fixtures/pageserver/http.py
+++ b/test_runner/fixtures/pageserver/http.py
@@ -839,6 +839,17 @@ class PageserverHttpClient(requests.Session, MetricsGetter):
res_json = res.json()
return res_json
+ def timeline_lsn_lease_dump(
+ self, tenant_shard_id: TenantShardId, timeline_id: TimelineId
+ ):
+ res = self.get(
+ f"http://localhost:{self.port}/v1/tenant/{tenant_shard_id}/timeline/{timeline_id}/lsn_lease",
+ )
+ self.verbose_error(res)
+ res_json = res.json()
+ return res_json
+
+
def timeline_lsn_lease(
self, tenant_id: TenantId | TenantShardId, timeline_id: TimelineId, lsn: Lsn
):
diff --git a/test_runner/regress/test_hot_standby.py b/test_runner/regress/test_hot_standby.py
index 552cb0a4d7..54bf89b0b2 100644
--- a/test_runner/regress/test_hot_standby.py
+++ b/test_runner/regress/test_hot_standby.py
@@ -200,6 +200,14 @@ def test_hot_standby_gc(neon_env_builder: NeonEnvBuilder, pause_apply: bool):
res = s_cur.fetchone()
assert res == (10000,)
+ for tenant_shard_id, pageserver in shards:
+ client = pageserver.http_client()
+ leases = client.timeline_lsn_lease_dump(tenant_shard_id, timeline_id)
+ leases_sorted = sorted(list(leases.items()))
+ log.info(f"lease state {tenant_shard_id=} {len(leases_sorted)} leases")
+ for lease in leases_sorted:
+ log.info(f"{lease=}")
+ assert len(leases) == 1
def run_pgbench(connstr: str, pg_bin: PgBin):
log.info(f"Start a pgbench workload on pg {connstr}")