perf: batch tenant summary response assembly
This commit is contained in:
@@ -82,6 +82,7 @@ TENANT_NON_STATUS_UPDATE_FIELDS = {
|
|||||||
"allow_api_keys",
|
"allow_api_keys",
|
||||||
}
|
}
|
||||||
TENANT_GOVERNANCE_OVERRIDE_FIELDS = ("allow_custom_groups", "allow_custom_roles", "allow_api_keys")
|
TENANT_GOVERNANCE_OVERRIDE_FIELDS = ("allow_custom_groups", "allow_custom_roles", "allow_api_keys")
|
||||||
|
TENANT_FULL_CURSOR_PREFIX = "full:tenants:"
|
||||||
|
|
||||||
|
|
||||||
def _tenant_access_provisioner() -> TenantAccessProvisioner:
|
def _tenant_access_provisioner() -> TenantAccessProvisioner:
|
||||||
@@ -141,7 +142,11 @@ def _tenant_item(session: Session, tenant: Tenant) -> TenantAdminItem:
|
|||||||
"allow_api_keys": governance.allow_api_keys,
|
"allow_api_keys": governance.allow_api_keys,
|
||||||
},
|
},
|
||||||
is_active=tenant.is_active,
|
is_active=tenant.is_active,
|
||||||
counts=tenant_counts(session, tenant.id),
|
counts=tenant_counts(
|
||||||
|
session,
|
||||||
|
tenant.id,
|
||||||
|
module_ids=("campaigns", "files"),
|
||||||
|
),
|
||||||
created_at=tenant.created_at,
|
created_at=tenant.created_at,
|
||||||
updated_at=tenant.updated_at,
|
updated_at=tenant.updated_at,
|
||||||
)
|
)
|
||||||
@@ -317,6 +322,50 @@ def _tenant_list_response_watermark(session: Session, *, entries, has_more: bool
|
|||||||
return encode_sequence_watermark(entries[-1].id) if has_more and entries else _tenant_list_watermark(session)
|
return encode_sequence_watermark(entries[-1].id) if has_more and entries else _tenant_list_watermark(session)
|
||||||
|
|
||||||
|
|
||||||
|
def _tenant_page(query, *, page: int, page_size: int):
|
||||||
|
total = query.order_by(None).count()
|
||||||
|
pages = max(1, (total + page_size - 1) // page_size)
|
||||||
|
items = query.offset((page - 1) * page_size).limit(page_size).all()
|
||||||
|
return items, {
|
||||||
|
"total": total,
|
||||||
|
"page": page,
|
||||||
|
"page_size": page_size,
|
||||||
|
"pages": pages,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _tenant_full_cursor(
|
||||||
|
*,
|
||||||
|
page: int,
|
||||||
|
snapshot_sequence: int,
|
||||||
|
) -> str:
|
||||||
|
return f"{TENANT_FULL_CURSOR_PREFIX}{int(page)}:{int(snapshot_sequence)}"
|
||||||
|
|
||||||
|
|
||||||
|
def _decode_tenant_full_cursor(value: str | None) -> tuple[int, int] | None:
|
||||||
|
if not value or not value.startswith(TENANT_FULL_CURSOR_PREFIX):
|
||||||
|
return None
|
||||||
|
parts = value[len(TENANT_FULL_CURSOR_PREFIX):].split(":", 1)
|
||||||
|
if len(parts) != 2:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid tenant full snapshot cursor",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
page, snapshot_sequence = (int(item) for item in parts)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid tenant full snapshot cursor",
|
||||||
|
) from exc
|
||||||
|
if page < 1 or snapshot_sequence < 0:
|
||||||
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_400_BAD_REQUEST,
|
||||||
|
detail="Invalid tenant full snapshot cursor",
|
||||||
|
)
|
||||||
|
return page, snapshot_sequence
|
||||||
|
|
||||||
|
|
||||||
def _tenant_list_deleted_entries(entries: list[ChangeSequenceEntry], visible_tenant_ids: set[str]):
|
def _tenant_list_deleted_entries(entries: list[ChangeSequenceEntry], visible_tenant_ids: set[str]):
|
||||||
return [
|
return [
|
||||||
{"id": entry.resource_id, "resource_type": entry.resource_type or TENANT_LIST_RESOURCE}
|
{"id": entry.resource_id, "resource_type": entry.resource_type or TENANT_LIST_RESOURCE}
|
||||||
@@ -397,21 +446,54 @@ def switch_tenant_context(
|
|||||||
|
|
||||||
@router.get("/tenants", response_model=TenantListResponse)
|
@router.get("/tenants", response_model=TenantListResponse)
|
||||||
def list_tenants(
|
def list_tenants(
|
||||||
|
page: int = Query(default=1, ge=1),
|
||||||
|
page_size: int = Query(default=100, ge=1, le=500),
|
||||||
session: Session = Depends(get_session),
|
session: Session = Depends(get_session),
|
||||||
principal: ApiPrincipal = Depends(require_scope("system:tenants:read")),
|
principal: ApiPrincipal = Depends(require_scope("system:tenants:read")),
|
||||||
):
|
):
|
||||||
tenants = session.query(Tenant).order_by(Tenant.name.asc()).all()
|
tenants, pagination = _tenant_page(
|
||||||
return TenantListResponse(tenants=[_tenant_item(session, tenant) for tenant in tenants])
|
session.query(Tenant).order_by(Tenant.name.asc(), Tenant.id.asc()),
|
||||||
|
page=page,
|
||||||
|
page_size=page_size,
|
||||||
|
)
|
||||||
|
return TenantListResponse(
|
||||||
|
tenants=[_tenant_item(session, tenant) for tenant in tenants],
|
||||||
|
**pagination,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _full_tenant_list_delta_response(session: Session) -> TenantListDeltaResponse:
|
def _full_tenant_list_delta_response(
|
||||||
tenants = session.query(Tenant).order_by(Tenant.name.asc()).all()
|
session: Session,
|
||||||
|
*,
|
||||||
|
cursor: tuple[int, int] | None = None,
|
||||||
|
limit: int = 100,
|
||||||
|
) -> TenantListDeltaResponse:
|
||||||
|
page = cursor[0] if cursor is not None else 1
|
||||||
|
snapshot_sequence = (
|
||||||
|
cursor[1]
|
||||||
|
if cursor is not None
|
||||||
|
else decode_sequence_watermark(_tenant_list_watermark(session))
|
||||||
|
)
|
||||||
|
tenants, pagination = _tenant_page(
|
||||||
|
session.query(Tenant).order_by(Tenant.name.asc(), Tenant.id.asc()),
|
||||||
|
page=page,
|
||||||
|
page_size=limit,
|
||||||
|
)
|
||||||
|
has_more = page < pagination["pages"]
|
||||||
return TenantListDeltaResponse(
|
return TenantListDeltaResponse(
|
||||||
tenants=[_tenant_item(session, tenant) for tenant in tenants],
|
tenants=[_tenant_item(session, tenant) for tenant in tenants],
|
||||||
deleted=[],
|
deleted=[],
|
||||||
watermark=_tenant_list_watermark(session),
|
watermark=(
|
||||||
has_more=False,
|
_tenant_full_cursor(
|
||||||
|
page=page + 1,
|
||||||
|
snapshot_sequence=snapshot_sequence,
|
||||||
|
)
|
||||||
|
if has_more
|
||||||
|
else encode_sequence_watermark(snapshot_sequence)
|
||||||
|
),
|
||||||
|
has_more=has_more,
|
||||||
full=True,
|
full=True,
|
||||||
|
**pagination,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -423,11 +505,16 @@ def list_tenants_delta(
|
|||||||
principal: ApiPrincipal = Depends(require_scope("system:tenants:read")),
|
principal: ApiPrincipal = Depends(require_scope("system:tenants:read")),
|
||||||
):
|
):
|
||||||
del principal
|
del principal
|
||||||
if since is None:
|
full_cursor = _decode_tenant_full_cursor(since)
|
||||||
return _full_tenant_list_delta_response(session)
|
if since is None or full_cursor is not None:
|
||||||
|
return _full_tenant_list_delta_response(
|
||||||
|
session,
|
||||||
|
cursor=full_cursor,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
entries, has_more = _tenant_list_delta_entries(session, since=since, limit=limit)
|
entries, has_more = _tenant_list_delta_entries(session, since=since, limit=limit)
|
||||||
if entries is None:
|
if entries is None:
|
||||||
return _full_tenant_list_delta_response(session)
|
return _full_tenant_list_delta_response(session, limit=limit)
|
||||||
changed_ids = [entry.resource_id for entry in entries if entry.resource_id and entry.operation != "deleted"]
|
changed_ids = [entry.resource_id for entry in entries if entry.resource_id and entry.operation != "deleted"]
|
||||||
tenants = []
|
tenants = []
|
||||||
if changed_ids:
|
if changed_ids:
|
||||||
@@ -439,6 +526,10 @@ def list_tenants_delta(
|
|||||||
watermark=_tenant_list_response_watermark(session, entries=entries, has_more=has_more),
|
watermark=_tenant_list_response_watermark(session, entries=entries, has_more=has_more),
|
||||||
has_more=has_more,
|
has_more=has_more,
|
||||||
full=False,
|
full=False,
|
||||||
|
total=len(tenants),
|
||||||
|
page=1,
|
||||||
|
page_size=limit,
|
||||||
|
pages=1,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ class TenantAdminItem(BaseModel):
|
|||||||
|
|
||||||
class TenantListResponse(BaseModel):
|
class TenantListResponse(BaseModel):
|
||||||
tenants: list[TenantAdminItem]
|
tenants: list[TenantAdminItem]
|
||||||
|
total: int = 0
|
||||||
|
page: int = 1
|
||||||
|
page_size: int = 100
|
||||||
|
pages: int = 1
|
||||||
|
|
||||||
|
|
||||||
class TenantListDeltaResponse(BaseModel):
|
class TenantListDeltaResponse(TenantListResponse):
|
||||||
tenants: list[TenantAdminItem] = Field(default_factory=list)
|
tenants: list[TenantAdminItem] = Field(default_factory=list)
|
||||||
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
|
deleted: list[DeltaDeletedItem] = Field(default_factory=list)
|
||||||
watermark: str | None = None
|
watermark: str | None = None
|
||||||
|
|||||||
Reference in New Issue
Block a user