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")
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 = {
"AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author),
"TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic),
@@ -68,6 +76,10 @@ async def follow(
follows: list[dict[str, Any]] = []
error: str | None = None
# ✅ Сохраняем entity_id и error вне сессии для использования после её закрытия
entity_id_result: int | None = None
error_result: str | None = None
try:
logger.debug("Попытка получить сущность из базы данных")
with local_session() as session:
@@ -110,15 +122,10 @@ async def follow(
.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:
logger.info(f"Пользователь {follower_id} уже подписан на {what.lower()} с ID {entity_id}")
error = "already following"
error_result = "already following"
# ✅ КРИТИЧНО: Не делаем return - продолжаем для получения списка подписок
else:
logger.debug("Добавление новой записи в базу данных")
sub = follower_class(follower=follower_id, **{entity_field: entity_id})
@@ -127,37 +134,36 @@ async def follow(
session.commit()
logger.info(f"Пользователь {follower_id} подписался на {what.lower()} с ID {entity_id}")
if cache_method:
logger.debug("Обновление кэша сущности")
await cache_method(entity_dict)
if cache_method:
logger.debug("Обновление кэша сущности")
await cache_method(entity_dict)
if what == "AUTHOR" and not existing_sub:
logger.debug("Отправка уведомления автору о подписке")
if isinstance(follower_dict, dict) and isinstance(entity_id, int):
# Получаем ID созданной записи подписки
subscription_id = getattr(sub, "id", None) if "sub" in locals() else None
await notify_follower(
follower=follower_dict,
author_id=entity_id,
action="follow",
subscription_id=subscription_id,
)
if what == "AUTHOR":
logger.debug("Отправка уведомления автору о подписке")
if isinstance(follower_dict, dict) and isinstance(entity_id, int):
# Получаем ID созданной записи подписки
subscription_id = getattr(sub, "id", None) if "sub" in locals() else None
await notify_follower(
follower=follower_dict,
author_id=entity_id,
action="follow",
subscription_id=subscription_id,
)
# Инвалидируем кеш статистики авторов для обновления счетчиков подписчиков
logger.debug("Инвалидируем кеш статистики авторов")
await invalidate_authors_cache(entity_id)
# Инвалидируем кеш статистики авторов для обновления счетчиков подписчиков
logger.debug("Инвалидируем кеш статистики авторов")
await invalidate_authors_cache(entity_id)
# ✅ КРИТИЧНО: Также инвалидируем кеш полных данных для корректной загрузки при рефреше
# Это гарантирует, что после рефреша клиент получит актуальные данные из БД
await redis.execute("DEL", f"author:id:{follower_id}")
logger.debug(f"Инвалидирован кеш полных данных пользователя: author:id:{follower_id}")
entity_id_result = entity_id
# Всегда получаем актуальный список подписок для возврата клиенту
# ✅ Получаем актуальный список подписок для возврата клиенту
# Кеш уже инвалидирован в начале функции, поэтому get_cached_follows_method
# вернет свежие данные из БД
if get_cached_follows_method and isinstance(follower_id, int):
logger.debug("Получение актуального списка подписок из кэша")
logger.debug("Получение актуального списка подписок после закрытия сессии")
existing_follows = await get_cached_follows_method(follower_id)
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)} элементов")
return {f"{entity_type}s": follows, "error": error}
return {f"{entity_type}s": follows, "error": error_result}
except Exception as exc:
logger.exception("Произошла ошибка в функции 'follow'")
@@ -207,6 +213,13 @@ async def unfollow(
follower_id = follower_dict.get("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 = {
"AUTHOR": (Author, AuthorFollower, get_cached_follower_authors, cache_author),
"TOPIC": (Topic, TopicFollower, get_cached_follower_topics, cache_topic),