storage controller: pagination for tenant listing API (#10365)

## Problem

For large deployments, the `control/v1/tenant` listing API can time out
transmitting a monolithic serialized response.

## Summary of changes

- Add `limit` and `start_after` parameters to listing API
- Update storcon_cli to use these parameters and limit requests to 1000
items at a time
This commit is contained in:
John Spray
2025-01-14 21:37:32 +00:00
committed by GitHub
parent 6debb49b87
commit 47c1640acc
5 changed files with 117 additions and 34 deletions

View File

@@ -653,6 +653,10 @@ async fn handle_tenant_list(
) -> Result<Response<Body>, ApiError> {
check_permissions(&req, Scope::Admin)?;
let limit: Option<usize> = parse_query_param(&req, "limit")?;
let start_after: Option<TenantId> = parse_query_param(&req, "start_after")?;
tracing::info!("start_after: {:?}", start_after);
match maybe_forward(req).await {
ForwardOutcome::Forwarded(res) => {
return res;
@@ -660,7 +664,7 @@ async fn handle_tenant_list(
ForwardOutcome::NotForwarded(_req) => {}
};
json_response(StatusCode::OK, service.tenant_list())
json_response(StatusCode::OK, service.tenant_list(limit, start_after))
}
async fn handle_node_register(req: Request<Body>) -> Result<Response<Body>, ApiError> {

View File

@@ -4158,17 +4158,42 @@ impl Service {
.ok_or_else(|| ApiError::NotFound(anyhow::anyhow!("Tenant {tenant_id} not found").into()))
}
pub(crate) fn tenant_list(&self) -> Vec<TenantDescribeResponse> {
/// limit & offset are pagination parameters. Since we are walking an in-memory HashMap, `offset` does not
/// avoid traversing data, it just avoid returning it. This is suitable for our purposes, since our in memory
/// maps are small enough to traverse fast, our pagination is just to avoid serializing huge JSON responses
/// in our external API.
pub(crate) fn tenant_list(
&self,
limit: Option<usize>,
start_after: Option<TenantId>,
) -> Vec<TenantDescribeResponse> {
let locked = self.inner.read().unwrap();
// Apply start_from parameter
let shard_range = match start_after {
None => locked.tenants.range(..),
Some(tenant_id) => locked.tenants.range(
TenantShardId {
tenant_id,
shard_number: ShardNumber(u8::MAX),
shard_count: ShardCount(u8::MAX),
}..,
),
};
let mut result = Vec::new();
for (_tenant_id, tenant_shards) in
&locked.tenants.iter().group_by(|(id, _shard)| id.tenant_id)
{
for (_tenant_id, tenant_shards) in &shard_range.group_by(|(id, _shard)| id.tenant_id) {
result.push(
self.tenant_describe_impl(tenant_shards.map(|(_k, v)| v))
.expect("Groups are always non-empty"),
);
// Enforce `limit` parameter
if let Some(limit) = limit {
if result.len() >= limit {
break;
}
}
}
result