follow-cache-invalidation-fix
All checks were successful
Deploy on push / deploy (push) Successful in 3m18s

This commit is contained in:
2025-10-01 23:41:28 +03:00
parent 50539a71ba
commit 2dacb837f3

View File

@@ -52,6 +52,14 @@ async def follow(
follower_id = follower_dict.get("id") follower_id = follower_dict.get("id")
logger.debug(f"follower_id: {follower_id}") logger.debug(f"follower_id: {follower_id}")
# ✅ КРИТИЧНО: Инвалидируем кеш В САМОМ НАЧАЛЕ, ДО любых операций
# чтобы предотвратить чтение старых данных при последующей перезагрузке
entity_type = what.lower()
cache_key_pattern = f"author:follows-{entity_type}s:{follower_id}"
await redis.execute("DEL", cache_key_pattern)
await redis.execute("DEL", f"author:id:{follower_id}")
logger.debug(f"Инвалидирован кеш подписок В НАЧАЛЕ операции: {cache_key_pattern}")
entity_classes = { entity_classes = {
"AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author), "AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author),
"TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic), "TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic),
@@ -68,6 +76,10 @@ async def follow(
follows: list[dict[str, Any]] = [] follows: list[dict[str, Any]] = []
error: str | None = None error: str | None = None
# ✅ Сохраняем entity_id и error вне сессии для использования после её закрытия
entity_id_result: int | None = None
error_result: str | None = None
try: try:
logger.debug("Попытка получить сущность из базы данных") logger.debug("Попытка получить сущность из базы данных")
with local_session() as session: with local_session() as session:
@@ -110,15 +122,10 @@ async def follow(
.first() .first()
) )
# 🔧 ИСПРАВЛЕНИЕ: Инвалидируем кэш ДО проверки existing_sub,
# чтобы всегда возвращать актуальный список подписок (даже при ошибке "already following")
cache_key_pattern = f"author:follows-{entity_type}s:{follower_id}"
await redis.execute("DEL", cache_key_pattern)
logger.debug(f"Инвалидирован кэш подписок: {cache_key_pattern}")
if existing_sub: if existing_sub:
logger.info(f"Пользователь {follower_id} уже подписан на {what.lower()} с ID {entity_id}") logger.info(f"Пользователь {follower_id} уже подписан на {what.lower()} с ID {entity_id}")
error = "already following" error_result = "already following"
# ✅ КРИТИЧНО: Не делаем return - продолжаем для получения списка подписок
else: else:
logger.debug("Добавление новой записи в базу данных") logger.debug("Добавление новой записи в базу данных")
sub = follower_class(follower=follower_id, **{entity_field: entity_id}) sub = follower_class(follower=follower_id, **{entity_field: entity_id})
@@ -131,7 +138,7 @@ async def follow(
logger.debug("Обновление кэша сущности") logger.debug("Обновление кэша сущности")
await cache_method(entity_dict) await cache_method(entity_dict)
if what == "AUTHOR" and not existing_sub: if what == "AUTHOR":
logger.debug("Отправка уведомления автору о подписке") logger.debug("Отправка уведомления автору о подписке")
if isinstance(follower_dict, dict) and isinstance(entity_id, int): if isinstance(follower_dict, dict) and isinstance(entity_id, int):
# Получаем ID созданной записи подписки # Получаем ID созданной записи подписки
@@ -147,17 +154,16 @@ async def follow(
logger.debug("Инвалидируем кеш статистики авторов") logger.debug("Инвалидируем кеш статистики авторов")
await invalidate_authors_cache(entity_id) await invalidate_authors_cache(entity_id)
# ✅ КРИТИЧНО: Также инвалидируем кеш полных данных для корректной загрузки при рефреше entity_id_result = entity_id
# Это гарантирует, что после рефреша клиент получит актуальные данные из БД
await redis.execute("DEL", f"author:id:{follower_id}")
logger.debug(f"Инвалидирован кеш полных данных пользователя: author:id:{follower_id}")
# Всегда получаем актуальный список подписок для возврата клиенту # ✅ Получаем актуальный список подписок для возврата клиенту
# Кеш уже инвалидирован в начале функции, поэтому get_cached_follows_method
# вернет свежие данные из БД
if get_cached_follows_method and isinstance(follower_id, int): if get_cached_follows_method and isinstance(follower_id, int):
logger.debug("Получение актуального списка подписок из кэша") logger.debug("Получение актуального списка подписок после закрытия сессии")
existing_follows = await get_cached_follows_method(follower_id) existing_follows = await get_cached_follows_method(follower_id)
logger.debug( logger.debug(
f"Получено подписок: {len(existing_follows)}, содержит target={entity_id in [f.get('id') for f in existing_follows] if existing_follows else False}" f"Получено подписок: {len(existing_follows)}, содержит target={entity_id_result in [f.get('id') for f in existing_follows] if existing_follows else False}"
) )
# Если это авторы, получаем безопасную версию # Если это авторы, получаем безопасную версию
@@ -181,7 +187,7 @@ async def follow(
logger.debug(f"Актуальный список подписок получен: {len(follows)} элементов") logger.debug(f"Актуальный список подписок получен: {len(follows)} элементов")
return {f"{entity_type}s": follows, "error": error} return {f"{entity_type}s": follows, "error": error_result}
except Exception as exc: except Exception as exc:
logger.exception("Произошла ошибка в функции 'follow'") logger.exception("Произошла ошибка в функции 'follow'")
@@ -207,6 +213,13 @@ async def unfollow(
follower_id = follower_dict.get("id") follower_id = follower_dict.get("id")
logger.debug(f"follower_id: {follower_id}") logger.debug(f"follower_id: {follower_id}")
# ✅ КРИТИЧНО: Инвалидируем кеш В САМОМ НАЧАЛЕ, ДО любых операций
entity_type = what.lower()
cache_key_pattern = f"author:follows-{entity_type}s:{follower_id}"
await redis.execute("DEL", cache_key_pattern)
await redis.execute("DEL", f"author:id:{follower_id}")
logger.debug(f"Инвалидирован кеш подписок В НАЧАЛЕ операции unfollow: {cache_key_pattern}")
entity_classes = { entity_classes = {
"AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author), "AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author),
"TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic), "TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic),