perf(organizations): cache and instrument API responses
This commit is contained in:
@@ -11,7 +11,8 @@ from django.db import connection
|
||||
from django.test import override_settings
|
||||
from django.test.utils import CaptureQueriesContext
|
||||
from django.urls import reverse
|
||||
from organizations.models import Organization
|
||||
from organizations.cache import invalidate_organization_api_cache
|
||||
from organizations.models import Organization, OrganizationDataSnapshot
|
||||
from organizations.services import OrganizationDataSnapshotRefreshService
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APITestCase
|
||||
@@ -104,20 +105,23 @@ class OrganizationsApiV2Test(APITestCase):
|
||||
"exclude_data",
|
||||
):
|
||||
self.assertIn(expected_name, list_parameters)
|
||||
self.assertIn("по умолчанию true", list_parameters["has_registry"]["description"])
|
||||
self.assertIn(
|
||||
"по умолчанию true", list_parameters["has_registry"]["description"]
|
||||
)
|
||||
self.assertIn(
|
||||
"industrial_products",
|
||||
list_parameters["data"]["description"],
|
||||
)
|
||||
detail_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in detail_operation["parameters"]
|
||||
parameter["name"]: parameter for parameter in detail_operation["parameters"]
|
||||
}
|
||||
self.assertEqual(detail_parameters["uid"]["type"], "string")
|
||||
self.assertEqual(detail_parameters["uid"]["format"], "uuid")
|
||||
self.assertIn("data", detail_parameters)
|
||||
self.assertIn("exclude_data", detail_parameters)
|
||||
self.assertIn("Пагинированный", list_operation["responses"]["200"]["description"])
|
||||
self.assertIn(
|
||||
"Пагинированный", list_operation["responses"]["200"]["description"]
|
||||
)
|
||||
self.assertIn(
|
||||
"Карточка организации",
|
||||
detail_operation["responses"]["200"]["description"],
|
||||
@@ -132,7 +136,9 @@ class OrganizationsApiV2Test(APITestCase):
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api_v2:organizations:organizations-detail", args=[organization.uid])
|
||||
reverse(
|
||||
"api_v2:organizations:organizations-detail", args=[organization.uid]
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -148,13 +154,100 @@ class OrganizationsApiV2Test(APITestCase):
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api_v2:organizations:organizations-detail", args=[organization.uid])
|
||||
reverse(
|
||||
"api_v2:organizations:organizations-detail", args=[organization.uid]
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["name"], 'АКЦИОНЕРНОЕ ОБЩЕСТВО "СЕВЕРНЫЙ МОСТ"')
|
||||
self.assertEqual(response.data["normalized_name"], 'АО "Северный Мост"')
|
||||
|
||||
def test_list_omits_full_snapshot_data_by_default_but_keeps_summary(self):
|
||||
organization = Organization.objects.create(
|
||||
name='ООО "Легкий список"',
|
||||
inn="7712345682",
|
||||
kpp="771201005",
|
||||
ogrn="1027700132200",
|
||||
)
|
||||
OrganizationDataSnapshot.objects.create(
|
||||
organization=organization,
|
||||
data={
|
||||
"industrial": [{"id": 1}],
|
||||
"fns_reports": [{"id": 2}, {"id": 3}],
|
||||
},
|
||||
registries=[],
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api_v2:organizations:organizations-list"),
|
||||
{"inn": organization.inn, "has_registry": "false"},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
item = response.data["data"][0]
|
||||
self.assertEqual(item["data"], {})
|
||||
self.assertEqual(
|
||||
item["data_sources"],
|
||||
[
|
||||
{"source": "fns_reports", "count": 2},
|
||||
{"source": "industrial", "count": 1},
|
||||
],
|
||||
)
|
||||
|
||||
def test_list_returns_snapshot_data_when_sources_are_requested(self):
|
||||
organization = Organization.objects.create(
|
||||
name='ООО "Явные данные"',
|
||||
inn="7712345683",
|
||||
kpp="771201006",
|
||||
ogrn="1027700132201",
|
||||
)
|
||||
OrganizationDataSnapshot.objects.create(
|
||||
organization=organization,
|
||||
data={
|
||||
"industrial": [{"id": 1}],
|
||||
"fns_reports": [{"id": 2}],
|
||||
},
|
||||
registries=[],
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api_v2:organizations:organizations-list"),
|
||||
{
|
||||
"inn": organization.inn,
|
||||
"has_registry": "false",
|
||||
"data": "industrial",
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(
|
||||
response.data["data"][0]["data"],
|
||||
{"industrial": [{"id": 1}]},
|
||||
)
|
||||
|
||||
def test_detail_keeps_full_snapshot_data_by_default(self):
|
||||
organization = Organization.objects.create(
|
||||
name='ООО "Полная карточка"',
|
||||
inn="7712345684",
|
||||
kpp="771201007",
|
||||
ogrn="1027700132202",
|
||||
)
|
||||
OrganizationDataSnapshot.objects.create(
|
||||
organization=organization,
|
||||
data={"industrial": [{"id": 1}]},
|
||||
registries=[],
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse(
|
||||
"api_v2:organizations:organizations-detail", args=[organization.uid]
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(response.data["data"], {"industrial": [{"id": 1}]})
|
||||
|
||||
def test_list_keeps_data_source_summary_when_data_payload_is_excluded(self):
|
||||
organization = Organization.objects.create(
|
||||
name='ООО "Сводка данных"',
|
||||
@@ -201,7 +294,9 @@ class OrganizationsApiV2Test(APITestCase):
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api_v2:organizations:organizations-detail", args=[organization.uid])
|
||||
reverse(
|
||||
"api_v2:organizations:organizations-detail", args=[organization.uid]
|
||||
)
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -299,6 +394,70 @@ class OrganizationsApiV2Test(APITestCase):
|
||||
self.assertEqual(different_query_response["X-Cache"], "MISS")
|
||||
self.assertEqual(different_query_response.data["data"][0]["inn"], "7744444444")
|
||||
|
||||
def test_list_response_cache_is_invalidated_by_version_bump(self):
|
||||
Organization.objects.create(
|
||||
name='ООО "Версия кеша"',
|
||||
inn="7744444445",
|
||||
kpp="774401002",
|
||||
ogrn="1027700132445",
|
||||
)
|
||||
url = reverse("api_v2:organizations:organizations-list")
|
||||
params = {"inn": "7744444445", "has_registry": "false"}
|
||||
|
||||
first_response = self.client.get(url, params)
|
||||
second_response = self.client.get(url, params)
|
||||
invalidate_organization_api_cache()
|
||||
third_response = self.client.get(url, params)
|
||||
|
||||
self.assertEqual(first_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(first_response["X-Cache"], "MISS")
|
||||
self.assertEqual(second_response["X-Cache"], "HIT")
|
||||
self.assertEqual(third_response["X-Cache"], "MISS")
|
||||
|
||||
def test_source_update_invalidates_organization_cache(self):
|
||||
Organization.objects.create(
|
||||
name='ООО "Источник сброса"',
|
||||
inn="7744444446",
|
||||
kpp="774401003",
|
||||
ogrn="1027700132446",
|
||||
)
|
||||
url = reverse("api_v2:organizations:organizations-list")
|
||||
params = {"inn": "7744444446", "has_registry": "false"}
|
||||
|
||||
first_response = self.client.get(url, params)
|
||||
second_response = self.client.get(url, params)
|
||||
with self.captureOnCommitCallbacks(execute=True):
|
||||
ParserLoadLog.objects.create(
|
||||
source=ParserLoadLog.Source.FSTEC,
|
||||
batch_id=1,
|
||||
status=ParserLoadLog.Status.SUCCESS,
|
||||
)
|
||||
third_response = self.client.get(url, params)
|
||||
|
||||
self.assertEqual(first_response["X-Cache"], "MISS")
|
||||
self.assertEqual(second_response["X-Cache"], "HIT")
|
||||
self.assertEqual(third_response["X-Cache"], "MISS")
|
||||
|
||||
def test_registry_update_invalidates_organization_cache(self):
|
||||
Organization.objects.create(
|
||||
name='ООО "Реестр сброса"',
|
||||
inn="7744444447",
|
||||
kpp="774401004",
|
||||
ogrn="1027700132447",
|
||||
)
|
||||
url = reverse("api_v2:organizations:organizations-list")
|
||||
params = {"inn": "7744444447", "has_registry": "false"}
|
||||
|
||||
first_response = self.client.get(url, params)
|
||||
second_response = self.client.get(url, params)
|
||||
with self.captureOnCommitCallbacks(execute=True):
|
||||
RegisterFactory(name="Реестр для сброса кеша")
|
||||
third_response = self.client.get(url, params)
|
||||
|
||||
self.assertEqual(first_response["X-Cache"], "MISS")
|
||||
self.assertEqual(second_response["X-Cache"], "HIT")
|
||||
self.assertEqual(third_response["X-Cache"], "MISS")
|
||||
|
||||
def test_retrieve_response_is_cached(self):
|
||||
organization = Organization.objects.create(
|
||||
name='ООО "Деталь"',
|
||||
@@ -306,7 +465,9 @@ class OrganizationsApiV2Test(APITestCase):
|
||||
kpp="775501001",
|
||||
ogrn="1027700132555",
|
||||
)
|
||||
url = reverse("api_v2:organizations:organizations-detail", args=[organization.uid])
|
||||
url = reverse(
|
||||
"api_v2:organizations:organizations-detail", args=[organization.uid]
|
||||
)
|
||||
|
||||
first_response = self.client.get(url)
|
||||
organization.name = 'ООО "Изменено"'
|
||||
@@ -417,7 +578,13 @@ class OrganizationsApiV2Test(APITestCase):
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api_v2:organizations:organizations-list"),
|
||||
{"inn": organization.inn},
|
||||
{
|
||||
"inn": organization.inn,
|
||||
"data": (
|
||||
"industrial,industrial_products,manufactures,inspections,"
|
||||
"procurements,procurements_44fz,fstec,fns_reports"
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
self.assertEqual(response.status_code, status.HTTP_200_OK)
|
||||
@@ -525,9 +692,7 @@ class OrganizationsApiV2Test(APITestCase):
|
||||
"period_end": 500,
|
||||
},
|
||||
)
|
||||
self.assertTrue(
|
||||
all(isinstance(value, list) for value in item["data"].values())
|
||||
)
|
||||
self.assertTrue(all(isinstance(value, list) for value in item["data"].values()))
|
||||
|
||||
def test_filters_by_registry_and_has_registry(self):
|
||||
with_registry = Organization.objects.create(
|
||||
@@ -608,7 +773,9 @@ class OrganizationsApiV2Test(APITestCase):
|
||||
|
||||
self.assertEqual(default_response.status_code, status.HTTP_200_OK)
|
||||
self.assertEqual(default_response.data["meta"]["pagination"]["total_count"], 1)
|
||||
self.assertEqual(default_response.data["data"][0]["uid"], str(with_registry.uid))
|
||||
self.assertEqual(
|
||||
default_response.data["data"][0]["uid"], str(with_registry.uid)
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
explicit_false_response.data["meta"]["pagination"]["total_count"],
|
||||
@@ -764,7 +931,9 @@ class OrganizationsApiV2Test(APITestCase):
|
||||
)
|
||||
|
||||
response = self.client.get(
|
||||
reverse("api_v2:organizations:organizations-detail", args=[organization.uid]),
|
||||
reverse(
|
||||
"api_v2:organizations:organizations-detail", args=[organization.uid]
|
||||
),
|
||||
{"data": "unknown"},
|
||||
)
|
||||
|
||||
@@ -823,7 +992,9 @@ class OrganizationsApiV2Test(APITestCase):
|
||||
OrganizationDataSnapshotRefreshService.refresh(
|
||||
organization_uids=[str(organization.uid)],
|
||||
)
|
||||
url = reverse("api_v2:organizations:organizations-detail", args=[organization.uid])
|
||||
url = reverse(
|
||||
"api_v2:organizations:organizations-detail", args=[organization.uid]
|
||||
)
|
||||
with CaptureQueriesContext(connection) as captured:
|
||||
response = self.client.get(
|
||||
url,
|
||||
|
||||
Reference in New Issue
Block a user