perf(organizations): cache and instrument API responses
This commit is contained in:
@@ -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]:
|
||||
|
||||
Reference in New Issue
Block a user