Skip to content

Commit

Permalink
add invalidate
Browse files Browse the repository at this point in the history
  • Loading branch information
bhnvx committed Feb 27, 2024
1 parent e6df24b commit df7da0c
Show file tree
Hide file tree
Showing 11 changed files with 1,103 additions and 966 deletions.
3 changes: 1 addition & 2 deletions app/apis/category/v1/home_category_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,10 @@
from app.dtos.category.coordinates_request import CoordinatesRequest
from app.services.category_service import (
get_distinct_home_categories,
get_home_categories_cached,
get_home_categories_one_by_one,
get_home_categories_cached
)


router = APIRouter(prefix="/v1/home_categories", tags=["Home Category"], redirect_slashes=False)


Expand Down
Empty file added app/dtos/shop/__init__.py
Empty file.
11 changes: 11 additions & 0 deletions app/dtos/shop/shop_creation_request.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import dataclasses

from app.entities.category.category_codes import CategoryCode
from app.entities.collections.geo_json import GeoJsonPolygon


@dataclasses.dataclass
class ShopCreationRequest:
name: str
category_codes: set[CategoryCode]
delivery_areas: list[GeoJsonPolygon]
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import asyncio
from abc import ABC, abstractmethod

from app.entities.collections.category_point.category_point_collection import (
CategoryPointCollection,
)
from app.entities.collections.category_point.category_point_document import (
CategoryPointDocument,
)
from app.entities.collections.shop.shop_document import ShopDocument
from app.entities.redis_repositories.category_point_redis_repository import (
CategoryPointRedisRepository,
)


class CategoryPointCacheInvalidator(ABC):
def __init__(self, shop: ShopDocument):
self._shop = shop

@abstractmethod
async def invalidate(self) -> None:
pass

async def _delete_cache(self, point: CategoryPointDocument) -> None:
"""
반드시 redis 에서 삭제가 성공한 후에 mongodb 에서 삭제해야 합니다.
mongodb 에서 삭제된 후에 redis 삭제가 실패할 경우, redis 캐시를 다시 찾아서 삭제할 방법이 없기 때문입니다.
"""
await CategoryPointRedisRepository.delete(point.cache_key)
await CategoryPointCollection.delete_by_id(point.id)


class ShopCreationCategoryPointCacheInvalidator(CategoryPointCacheInvalidator):
async def invalidate(self) -> None:
list_of_point_tuple = await self._get_list_of_point_tuple()
deleted = set()
for tp in list_of_point_tuple:
for point in tp:
if point.id not in deleted:
await self._delete_cache(point)
deleted.add(point.id)

async def _get_list_of_point_tuple(self) -> list[tuple[CategoryPointDocument, ...]]:
return await asyncio.gather(
*(
CategoryPointCollection.get_all_within_polygon_and_code_ne(area.poly, code)
for area in self._shop.delivery_areas
for code in self._shop.category_codes
)
)
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
from dataclasses import asdict

import pymongo
from bson import ObjectId
from motor.motor_asyncio import AsyncIOMotorCollection
from pymongo import ReturnDocument
from pymongo.errors import DuplicateKeyError
from pymongo.results import DeleteResult

from app.entities.category.category_codes import CategoryCode
from app.entities.collections.category_point.category_point_document import (
CategoryPointDocument,
)
from app.entities.collections.geo_json import GeoJsonPoint
from app.entities.collections.geo_json import GeoJsonPoint, GeoJsonPolygon
from app.utils.mongo import db


Expand Down Expand Up @@ -51,3 +53,27 @@ async def insert_or_replace(
cache_key=cache_key,
_id=_id,
)

@classmethod
async def delete_by_id(cls, _id: ObjectId) -> int:
result: DeleteResult = await cls._collection.delete_one({"_id": _id})
return result.deleted_count

@classmethod
async def get_all_within_polygon_and_code_ne(
cls, polygon: GeoJsonPolygon, code: CategoryCode
) -> tuple[CategoryPointDocument, ...]:
return tuple(
CategoryPointDocument(
cache_key=result["cache_key"],
codes=tuple(CategoryCode(code) for code in result["codes"]),
point=GeoJsonPoint(coordinates=result["point"]["coordinates"]),
_id=result["_id"],
)
for result in await cls._collection.find(
{
"point": {"$geoWithin": {"$geometry": asdict(polygon)}},
"codes": {"$ne": code.value},
}
).to_list(length=None)
)
2 changes: 1 addition & 1 deletion app/services/category_service.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import asyncio

from app.entities.caches.category_point.category_point_cache import CategoryPointCache
from app.entities.category.categories import CATEGORIES, Category
from app.entities.category.category_codes import CategoryCode
from app.entities.collections.geo_json import GeoJsonPoint
from app.entities.collections.shop.shop_collection import ShopCollection
from app.entities.caches.category_point.category_point_cache import CategoryPointCache


async def get_distinct_home_categories(longitude: float, latitude: float) -> tuple[Category, ...]:
Expand Down
21 changes: 21 additions & 0 deletions app/services/shop_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from app.dtos.shop.shop_creation_request import ShopCreationRequest
from app.entities.caches.category_point.category_point_cache_invalidator import (
ShopCreationCategoryPointCacheInvalidator,
)
from app.entities.collections import ShopCollection
from app.entities.collections.shop.shop_document import (
ShopDeliveryAreaSubDocument,
ShopDocument,
)


async def create_shop(shop_creation_request: ShopCreationRequest) -> ShopDocument:
shop = await ShopCollection.insert_one(
shop_creation_request.name,
list(shop_creation_request.category_codes),
[ShopDeliveryAreaSubDocument(poly=area) for area in shop_creation_request.delivery_areas],
)

await ShopCreationCategoryPointCacheInvalidator(shop).invalidate()

return shop
9 changes: 7 additions & 2 deletions app/tests/entities/caches/test_category_point_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,14 @@ async def test_category_cache_create_category_point() -> None:

# Then
assert point["cache_key"] == category_point_cache.cache_key
assert set(point["codes"]) == {CategoryCode.CHICKEN.value, CategoryCode.BURGER.value}
assert set(point["codes"]) == {
CategoryCode.CHICKEN.value,
CategoryCode.BURGER.value,
}
redis_result = await redis.get(category_point_cache.cache_key)
assert set(redis_result.split(",")) == {"chicken", "burger"}

if redis_result:
assert set(redis_result.split(",")) == {"chicken", "burger"}


async def test_category_cache_when_no_categories_then_it_creates_empty_category_point() -> None:
Expand Down
64 changes: 64 additions & 0 deletions app/tests/services/test_shop_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
from app.dtos.shop.shop_creation_request import ShopCreationRequest
from app.entities.category.category_codes import CategoryCode
from app.entities.collections import ShopCollection
from app.entities.collections.category_point.category_point_collection import (
CategoryPointCollection,
)
from app.entities.collections.geo_json import GeoJsonPoint, GeoJsonPolygon
from app.entities.collections.shop.shop_document import ShopDeliveryAreaSubDocument
from app.services.shop_service import create_shop


async def test_create_shop_invalidates_point() -> None:
# Given
await ShopCollection.insert_one(
name="sandwich_pizza_shop",
category_codes=[CategoryCode.SANDWICH, CategoryCode.PIZZA],
delivery_areas=[
ShopDeliveryAreaSubDocument(poly=GeoJsonPolygon(coordinates=[[[0, 0], [0, 10], [10, 10], [10, 0], [0, 0]]]))
],
)
shop_creation_request = ShopCreationRequest(
name="chicken_sandwich_shop",
category_codes={CategoryCode.CHICKEN, CategoryCode.SANDWICH},
delivery_areas=[GeoJsonPolygon(coordinates=[[[8, 0], [8, 14], [14, 14], [14, 0], [8, 0]]])],
)
not_be_removed1 = await CategoryPointCollection.insert_or_replace(
"3_3",
GeoJsonPoint(coordinates=[3, 3]),
(
CategoryCode.SANDWICH,
CategoryCode.PIZZA,
),
)
to_be_removed = await CategoryPointCollection.insert_or_replace(
"9_9",
GeoJsonPoint(coordinates=[9, 9]),
(
CategoryCode.SANDWICH,
CategoryCode.PIZZA,
),
)
not_be_removed2 = await CategoryPointCollection.insert_or_replace(
"9.2_9.2",
GeoJsonPoint(coordinates=[9.2, 9.2]),
(
CategoryCode.SANDWICH,
CategoryCode.PIZZA,
CategoryCode.CHICKEN,
),
)

# When
shop = await create_shop(shop_creation_request)

# Then
result = await CategoryPointCollection._collection.find({}, {"_id": True}).to_list(None)

assert len(result) == 2
ids = [item["_id"] for item in result]
assert not_be_removed1.id in ids
assert not_be_removed2.id in ids

result_shop = await ShopCollection._collection.find_one({"_id": shop.id})
assert shop.name == result_shop["name"]
Loading

0 comments on commit df7da0c

Please sign in to comment.