perf(organizations): cache and instrument API responses

This commit is contained in:
2026-05-14 16:14:45 +02:00
parent 6d1ec2e55c
commit 5fdd23ecc0
13 changed files with 603 additions and 33 deletions

View File

@@ -23,11 +23,15 @@ from organizations.api_enrichment import (
to_api_data_source,
to_internal_data_source,
)
from organizations.cache import (
DEFAULT_ORGANIZATION_API_CACHE_TIMEOUT_SECONDS,
ORGANIZATION_API_CACHE_PREFIX,
get_organization_api_cache_version,
)
from organizations.filters import OrganizationFilter
from organizations.models import Organization
from organizations.serializers import OrganizationSerializer
ORGANIZATIONS_API_CACHE_TIMEOUT_SECONDS = 300
ORGANIZATIONS_TAG = swagger_tag("Организации", "Organizations")
ORGANIZATION_DATA_SOURCE_KEYS = ", ".join(sorted(API_DATA_SOURCE_KEY_SET))
@@ -59,7 +63,9 @@ ORGANIZATION_DATA_PARAMS = [
description=(
"Ограничить блок data одним или несколькими источниками. "
f"Допустимые значения: {ORGANIZATION_DATA_SOURCE_KEYS}. "
"Можно передать несколько параметров или CSV-строку."
"Можно передать несколько параметров или CSV-строку. "
"На list endpoint блок data по умолчанию пустой; передайте этот "
"параметр, чтобы вернуть данные источников."
),
),
_query_parameter(
@@ -238,17 +244,28 @@ ORGANIZATION_DETAIL_RESPONSE = openapi.Response(
class CachedReadOnlyMixin:
"""Cache successful GET list/retrieve responses by full request path."""
cache_timeout = ORGANIZATIONS_API_CACHE_TIMEOUT_SECONDS
cache_key_prefix = "api:v2:organizations"
cache_timeout = DEFAULT_ORGANIZATION_API_CACHE_TIMEOUT_SECONDS
cache_key_prefix = ORGANIZATION_API_CACHE_PREFIX
def _cache_timeout(self) -> int:
return getattr(
settings,
"ORGANIZATIONS_API_CACHE_TIMEOUT_SECONDS",
self.cache_timeout,
)
def _build_cache_key(self, request) -> str:
user_marker = "anonymous"
if request.user and request.user.is_authenticated:
user_marker = "authenticated"
raw_key = f"{request.method}:{request.get_full_path()}:{user_marker}"
cache_version = get_organization_api_cache_version()
raw_key = (
f"v{cache_version}:{request.method}:"
f"{request.get_full_path()}:{user_marker}"
)
digest = hashlib.md5(raw_key.encode(), usedforsecurity=False).hexdigest()
return f"{self.cache_key_prefix}:{digest}"
return f"{self.cache_key_prefix}:v{cache_version}:{digest}"
def _cached_response(self, request, producer) -> Response:
cache_key = self._build_cache_key(request)
@@ -260,7 +277,7 @@ class CachedReadOnlyMixin:
response = producer()
if 200 <= response.status_code < 300:
cache.set(cache_key, response.data, timeout=self.cache_timeout)
cache.set(cache_key, response.data, timeout=self._cache_timeout())
response["X-Cache"] = "MISS"
return response
@@ -312,7 +329,9 @@ class OrganizationViewSet(CachedReadOnlyMixin, ReadOnlyModelViewSet):
"По умолчанию показывает только организации с активным участием "
"в реестрах; передайте has_registry=false, чтобы снять это ограничение. "
"Поддерживает пагинацию, поиск по наименованию и реквизитам, фильтры "
"по реестрам и наличию данных по источникам."
"по реестрам и наличию данных по источникам. Для list endpoint "
"тяжелый блок data по умолчанию пустой; передайте data/data_sources, "
"чтобы вернуть данные конкретных источников."
),
manual_parameters=ORGANIZATION_LIST_PARAMS,
responses={200: ORGANIZATION_LIST_RESPONSE},
@@ -343,7 +362,7 @@ class OrganizationViewSet(CachedReadOnlyMixin, ReadOnlyModelViewSet):
def _list_with_enrichment(self, request, *args: Any, **kwargs: Any) -> Response:
queryset = self.filter_queryset(self.get_queryset())
data_sources = self._parse_data_sources(request)
data_sources = self._parse_data_sources(request, default=set())
page = self.paginate_queryset(queryset)
if page is not None:
@@ -419,7 +438,11 @@ class OrganizationViewSet(CachedReadOnlyMixin, ReadOnlyModelViewSet):
)
@staticmethod
def _parse_data_sources(request) -> set[str] | None:
def _parse_data_sources(
request,
*,
default: set[str] | None = None,
) -> set[str] | None:
included = _query_param_values(request, "data", "data_sources")
excluded = _query_param_values(request, "exclude_data", "exclude_data_sources")
@@ -443,7 +466,7 @@ class OrganizationViewSet(CachedReadOnlyMixin, ReadOnlyModelViewSet):
}
if excluded:
return API_DATA_SOURCE_KEY_SET - excluded
return None
return default
def _query_param_values(request, *names: str) -> set[str]: