Add in-memory read cache with CUD-based invalidation

This commit is contained in:
Chris
2026-04-03 02:32:38 +08:00
parent e912d1498e
commit 55e640f2fb
6 changed files with 194 additions and 20 deletions

View File

@@ -53,6 +53,7 @@ from app.security.admin_guard import require_admin_principal
from app.security.api_client_auth import hash_api_key
from app.services.idp_admin_service import ProviderAdminService
from app.services.idp_catalog_sync import sync_from_provider
from app.services.runtime_cache import runtime_cache
from app.core.config import get_settings
router = APIRouter(
@@ -161,9 +162,16 @@ def list_companies(
limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0),
) -> ListResponse:
cache_key = f"admin:companies:{keyword or ''}:{limit}:{offset}"
cached = runtime_cache.get(cache_key)
if isinstance(cached, ListResponse):
return cached
repo = CompaniesRepository(db)
items, total = repo.list(keyword=keyword, limit=limit, offset=offset)
return ListResponse(items=[_company_item(i) for i in items], total=total, limit=limit, offset=offset)
result = ListResponse(items=[_company_item(i) for i in items], total=total, limit=limit, offset=offset)
runtime_cache.set(cache_key, result, ttl_seconds=20)
return result
@router.post("/companies", response_model=CompanyItem)
@@ -251,6 +259,11 @@ def list_sites(
limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0),
) -> ListResponse:
cache_key = f"admin:sites:{keyword or ''}:{company_key or ''}:{limit}:{offset}"
cached = runtime_cache.get(cache_key)
if isinstance(cached, ListResponse):
return cached
companies_repo = CompaniesRepository(db)
sites_repo = SitesRepository(db)
company_id = None
@@ -264,7 +277,9 @@ def list_sites(
company_map = {c.id: c for c in companies}
items, total = sites_repo.list(keyword=keyword, company_id=company_id, limit=limit, offset=offset)
response_items = [_site_item(i, company_map[i.company_id]) for i in items if i.company_id in company_map]
return ListResponse(items=response_items, total=total, limit=limit, offset=offset)
result = ListResponse(items=response_items, total=total, limit=limit, offset=offset)
runtime_cache.set(cache_key, result, ttl_seconds=20)
return result
@router.post("/sites", response_model=SiteItem)
@@ -375,9 +390,16 @@ def list_systems(
limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0),
) -> ListResponse:
cache_key = f"admin:systems:{keyword or ''}:{status_filter or ''}:{limit}:{offset}"
cached = runtime_cache.get(cache_key)
if isinstance(cached, ListResponse):
return cached
repo = SystemsRepository(db)
items, total = repo.list(keyword=keyword, status=status_filter, limit=limit, offset=offset)
return ListResponse(items=[_system_item(i) for i in items], total=total, limit=limit, offset=offset)
result = ListResponse(items=[_system_item(i) for i in items], total=total, limit=limit, offset=offset)
runtime_cache.set(cache_key, result, ttl_seconds=20)
return result
@router.post("/systems", response_model=SystemItem)
@@ -404,6 +426,11 @@ def list_roles(
limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0),
) -> ListResponse:
cache_key = f"admin:roles:{keyword or ''}:{system_key or ''}:{status_filter or ''}:{limit}:{offset}"
cached = runtime_cache.get(cache_key)
if isinstance(cached, ListResponse):
return cached
systems_repo = SystemsRepository(db)
roles_repo = RolesRepository(db)
@@ -433,7 +460,9 @@ def list_roles(
for row in rows
if row.system_id in system_map
]
return ListResponse(items=items, total=total, limit=limit, offset=offset)
result = ListResponse(items=items, total=total, limit=limit, offset=offset)
runtime_cache.set(cache_key, result, ttl_seconds=20)
return result
@router.post("/roles", response_model=RoleItem)
@@ -751,9 +780,17 @@ def list_members(
limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0),
) -> ListResponse:
is_active_key = "" if is_active is None else ("1" if is_active else "0")
cache_key = f"admin:members:{keyword or ''}:{is_active_key}:{limit}:{offset}"
cached = runtime_cache.get(cache_key)
if isinstance(cached, ListResponse):
return cached
repo = UsersRepository(db)
rows, total = repo.list(keyword=keyword, is_active=is_active, limit=limit, offset=offset)
return ListResponse(items=[_member_item(r) for r in rows], total=total, limit=limit, offset=offset)
result = ListResponse(items=[_member_item(r) for r in rows], total=total, limit=limit, offset=offset)
runtime_cache.set(cache_key, result, ttl_seconds=20)
return result
@router.post("/members", response_model=MemberItem)
@@ -950,14 +987,21 @@ def list_api_clients(
limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0),
) -> ListResponse:
cache_key = f"admin:api_clients:{keyword or ''}:{status_filter or ''}:{limit}:{offset}"
cached = runtime_cache.get(cache_key)
if isinstance(cached, ListResponse):
return cached
repo = ApiClientsRepository(db)
items, total = repo.list(keyword=keyword, status=status_filter, limit=limit, offset=offset)
return ListResponse(
result = ListResponse(
items=[ApiClientItem.model_validate(i, from_attributes=True) for i in items],
total=total,
limit=limit,
offset=offset,
)
runtime_cache.set(cache_key, result, ttl_seconds=20)
return result
@router.post("/sync/from-provider")

View File

@@ -12,6 +12,7 @@ from app.schemas.users import UserUpsertBySubRequest
from app.security.api_client_auth import require_api_client
from app.services.idp_admin_service import ProviderAdminService
from app.services.permission_service import PermissionService
from app.services.runtime_cache import runtime_cache
router = APIRouter(prefix="/internal", tags=["internal"], dependencies=[Depends(require_api_client)])
@@ -68,8 +69,13 @@ def _build_user_role_rows(db: Session, user_sub: str) -> list[tuple[str, str, st
@router.get("/users/{user_sub}/roles", response_model=InternalUserRoleResponse)
def get_user_roles(user_sub: str, db: Session = Depends(get_db)) -> InternalUserRoleResponse:
cache_key = f"internal:user_roles:{user_sub}"
cached = runtime_cache.get(cache_key)
if isinstance(cached, InternalUserRoleResponse):
return cached
rows = _build_user_role_rows(db, user_sub)
return InternalUserRoleResponse(
result = InternalUserRoleResponse(
user_sub=user_sub,
roles=[
InternalUserRoleItem(
@@ -94,6 +100,8 @@ def get_user_roles(user_sub: str, db: Session = Depends(get_db)) -> InternalUser
) in rows
],
)
runtime_cache.set(cache_key, result, ttl_seconds=30)
return result
@router.get("/permissions/{user_sub}/snapshot", response_model=RoleSnapshotResponse)
@@ -101,8 +109,15 @@ def get_permission_snapshot(
user_sub: str,
db: Session = Depends(get_db),
) -> RoleSnapshotResponse:
cache_key = f"internal:permissions_snapshot:{user_sub}"
cached = runtime_cache.get(cache_key)
if isinstance(cached, RoleSnapshotResponse):
return cached
rows = _build_user_role_rows(db, user_sub)
return PermissionService.build_role_snapshot(user_sub=user_sub, rows=rows)
result = PermissionService.build_role_snapshot(user_sub=user_sub, rows=rows)
runtime_cache.set(cache_key, result, ttl_seconds=30)
return result
@router.post("/provider/users/ensure", response_model=ProviderEnsureUserResponse)

View File

@@ -9,6 +9,7 @@ from app.schemas.auth import ProviderPrincipal, MeSummaryResponse
from app.schemas.permissions import RoleSnapshotResponse
from app.security.idp_jwt import require_authenticated_principal
from app.services.permission_service import PermissionService
from app.services.runtime_cache import runtime_cache
router = APIRouter(prefix="/me", tags=["me"])
@@ -18,6 +19,10 @@ def get_me(
principal: ProviderPrincipal = Depends(require_authenticated_principal),
db: Session = Depends(get_db),
) -> MeSummaryResponse:
cache_key = f"me:{principal.sub}"
cached = runtime_cache.get(cache_key)
if isinstance(cached, MeSummaryResponse):
return cached
try:
users_repo = UsersRepository(db)
user = users_repo.upsert_by_sub(
@@ -28,13 +33,17 @@ def get_me(
is_active=True,
status="active",
)
return MeSummaryResponse(sub=user.user_sub, email=user.email, display_name=user.display_name)
result = MeSummaryResponse(sub=user.user_sub, email=user.email, display_name=user.display_name)
runtime_cache.set(cache_key, result, ttl_seconds=30)
return result
except SQLAlchemyError:
return MeSummaryResponse(
result = MeSummaryResponse(
sub=principal.sub,
email=principal.email,
display_name=principal.name or principal.preferred_username,
)
runtime_cache.set(cache_key, result, ttl_seconds=15)
return result
@router.get("/permissions/snapshot", response_model=RoleSnapshotResponse)
@@ -42,6 +51,10 @@ def get_my_permission_snapshot(
principal: ProviderPrincipal = Depends(require_authenticated_principal),
db: Session = Depends(get_db),
) -> RoleSnapshotResponse:
cache_key = f"me:permissions_snapshot:{principal.sub}"
cached = runtime_cache.get(cache_key)
if isinstance(cached, RoleSnapshotResponse):
return cached
try:
users_repo = UsersRepository(db)
user_sites_repo = UserSitesRepository(db)
@@ -68,6 +81,10 @@ def get_my_permission_snapshot(
)
for site, company, role, system in rows
]
return PermissionService.build_role_snapshot(user_sub=principal.sub, rows=serialized)
result = PermissionService.build_role_snapshot(user_sub=principal.sub, rows=serialized)
runtime_cache.set(cache_key, result, ttl_seconds=30)
return result
except SQLAlchemyError:
return RoleSnapshotResponse(user_sub=principal.sub, roles=[])
result = RoleSnapshotResponse(user_sub=principal.sub, roles=[])
runtime_cache.set(cache_key, result, ttl_seconds=10)
return result