diff --git a/safekeeper/src/http/routes.rs b/safekeeper/src/http/routes.rs
index c9defb0bcf..d11815f6ef 100644
--- a/safekeeper/src/http/routes.rs
+++ b/safekeeper/src/http/routes.rs
@@ -114,6 +114,16 @@ fn check_permission(request: &Request
, tenant_id: Option) -> Res
})
}
+/// List all (not deleted) timelines.
+async fn timeline_list_handler(request: Request) -> Result, ApiError> {
+ check_permission(&request, None)?;
+ let res: Vec = GlobalTimelines::get_all()
+ .iter()
+ .map(|tli| tli.ttid)
+ .collect();
+ json_response(StatusCode::OK, res)
+}
+
/// Report info about timeline.
async fn timeline_status_handler(request: Request) -> Result, ApiError> {
let ttid = TenantTimelineId::new(
@@ -562,6 +572,9 @@ pub fn make_router(conf: SafeKeeperConf) -> RouterBuilder
.post("/v1/tenant/timeline", |r| {
request_span(r, timeline_create_handler)
})
+ .get("/v1/tenant/timeline", |r| {
+ request_span(r, timeline_list_handler)
+ })
.get("/v1/tenant/:tenant_id/timeline/:timeline_id", |r| {
request_span(r, timeline_status_handler)
})
diff --git a/test_runner/fixtures/common_types.py b/test_runner/fixtures/common_types.py
index b63dfd4e47..7cadcbb4c2 100644
--- a/test_runner/fixtures/common_types.py
+++ b/test_runner/fixtures/common_types.py
@@ -1,7 +1,7 @@
import random
from dataclasses import dataclass
from functools import total_ordering
-from typing import Any, Type, TypeVar, Union
+from typing import Any, Dict, Type, TypeVar, Union
T = TypeVar("T", bound="Id")
@@ -147,6 +147,19 @@ class TimelineId(Id):
return self.id.hex()
+@dataclass
+class TenantTimelineId:
+ tenant_id: TenantId
+ timeline_id: TimelineId
+
+ @classmethod
+ def from_json(cls, d: Dict[str, Any]) -> "TenantTimelineId":
+ return TenantTimelineId(
+ tenant_id=TenantId(d["tenant_id"]),
+ timeline_id=TimelineId(d["timeline_id"]),
+ )
+
+
# Workaround for compat with python 3.9, which does not have `typing.Self`
TTenantShardId = TypeVar("TTenantShardId", bound="TenantShardId")
diff --git a/test_runner/fixtures/safekeeper/http.py b/test_runner/fixtures/safekeeper/http.py
index a51b89744b..dd3a0a3d54 100644
--- a/test_runner/fixtures/safekeeper/http.py
+++ b/test_runner/fixtures/safekeeper/http.py
@@ -5,7 +5,7 @@ from typing import Any, Dict, List, Optional, Tuple, Union
import pytest
import requests
-from fixtures.common_types import Lsn, TenantId, TimelineId
+from fixtures.common_types import Lsn, TenantId, TenantTimelineId, TimelineId
from fixtures.log_helper import log
from fixtures.metrics import Metrics, MetricsGetter, parse_metrics
@@ -144,6 +144,12 @@ class SafekeeperHttpClient(requests.Session, MetricsGetter):
assert isinstance(res_json, dict)
return res_json
+ def timeline_list(self) -> List[TenantTimelineId]:
+ res = self.get(f"http://localhost:{self.port}/v1/tenant/timeline")
+ res.raise_for_status()
+ resj = res.json()
+ return [TenantTimelineId.from_json(ttidj) for ttidj in resj]
+
def timeline_create(
self,
tenant_id: TenantId,
diff --git a/test_runner/regress/test_wal_acceptor.py b/test_runner/regress/test_wal_acceptor.py
index 5d3b263936..bb3b16f3e1 100644
--- a/test_runner/regress/test_wal_acceptor.py
+++ b/test_runner/regress/test_wal_acceptor.py
@@ -254,6 +254,10 @@ def test_many_timelines(neon_env_builder: NeonEnvBuilder):
assert max(init_m[2].flush_lsns) <= min(final_m[2].flush_lsns) < middle_lsn
assert max(init_m[2].commit_lsns) <= min(final_m[2].commit_lsns) < middle_lsn
+ # Test timeline_list endpoint.
+ http_cli = env.safekeepers[0].http_client()
+ assert len(http_cli.timeline_list()) == 3
+
# Check that dead minority doesn't prevent the commits: execute insert n_inserts
# times, with fault_probability chance of getting a wal acceptor down or up