diff --git a/app/database/crud/subscription.py b/app/database/crud/subscription.py index da0d8d2d..ae65112a 100644 --- a/app/database/crud/subscription.py +++ b/app/database/crud/subscription.py @@ -6,6 +6,7 @@ from typing import Optional from sqlalchemy import and_, delete, func, select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload +from sqlalchemy.orm.exc import StaleDataError from app.config import settings from app.database.crud.notification import clear_notifications @@ -625,6 +626,9 @@ async def decrement_subscription_server_counts( if not subscription: return + # Save ID before any DB operations that might invalidate the ORM object + sub_id = subscription.id + server_ids: set[int] = set() if subscription_servers is not None: @@ -633,12 +637,12 @@ async def decrement_subscription_server_counts( server_ids.add(sub_server.server_squad_id) else: try: - ids_from_links = await get_subscription_server_ids(db, subscription.id) + ids_from_links = await get_subscription_server_ids(db, sub_id) server_ids.update(ids_from_links) except Exception as error: logger.error( '⚠️ Не удалось получить серверы подписки %s для уменьшения счетчика: %s', - subscription.id, + sub_id, error, ) @@ -652,7 +656,7 @@ async def decrement_subscription_server_counts( except Exception as error: logger.error( '⚠️ Не удалось сопоставить сквады подписки %s с серверами: %s', - subscription.id, + sub_id, error, ) @@ -663,11 +667,18 @@ async def decrement_subscription_server_counts( from app.database.crud.server_squad import remove_user_from_servers await remove_user_from_servers(db, sorted(server_ids)) + except StaleDataError: + logger.warning( + '⚠️ Подписка %s уже удалена (StaleDataError), пропускаем декремент серверов %s', + sub_id, + list(server_ids), + ) + await db.rollback() except Exception as error: logger.error( '⚠️ Ошибка уменьшения счетчика пользователей серверов %s для подписки %s: %s', list(server_ids), - subscription.id, + sub_id, error, ) diff --git a/app/services/remnawave_webhook_service.py b/app/services/remnawave_webhook_service.py index 9a62149d..c19bcb74 100644 --- a/app/services/remnawave_webhook_service.py +++ b/app/services/remnawave_webhook_service.py @@ -574,18 +574,43 @@ class RemnaWaveWebhookService: async def _handle_user_deleted( self, db: AsyncSession, user: User, subscription: Subscription | None, data: dict ) -> None: + user_id = user.id + sub_id = subscription.id if subscription else None + if subscription: self._stamp_webhook_update(subscription) # Decrement server counters BEFORE clearing connected_squads await decrement_subscription_server_counts(db, subscription) + # Re-fetch after potential rollback inside decrement_subscription_server_counts + try: + await db.refresh(subscription) + except Exception: + # Subscription was cascade-deleted, re-fetch user and skip subscription updates + logger.warning( + 'Webhook: subscription %s already deleted for user %s, skipping subscription cleanup', + sub_id, + user_id, + ) + subscription = None + try: + await db.refresh(user) + except Exception: + from app.database.crud.user import get_user_by_id + + user = await get_user_by_id(db, user_id) + if not user: + logger.error('Webhook: user %s not found after rollback', user_id) + return + + if subscription: if subscription.status != SubscriptionStatus.EXPIRED.value: subscription.status = SubscriptionStatus.EXPIRED.value logger.info( 'Webhook: subscription %s marked expired (user deleted in panel) for user %s', - subscription.id, - user.id, + sub_id, + user_id, ) # Clear subscription data — panel user no longer exists @@ -596,7 +621,7 @@ class RemnaWaveWebhookService: subscription.updated_at = datetime.now(UTC).replace(tzinfo=None) # Remove SubscriptionServer link rows - await db.execute(delete(SubscriptionServer).where(SubscriptionServer.subscription_id == subscription.id)) + await db.execute(delete(SubscriptionServer).where(SubscriptionServer.subscription_id == sub_id)) # Clear remnawave linkage if user.remnawave_uuid: