Merge pull request #2734 from BEDOLAGA-DEV/dev

Dev
This commit is contained in:
c0mrade
2026-03-14 00:17:06 +03:00
committed by GitHub
3 changed files with 41 additions and 7 deletions
+33 -3
View File
@@ -415,9 +415,19 @@ async def lock_user_for_update(db: AsyncSession, user: User) -> User:
Returns the refreshed user object with current DB values.
Must be called within an active transaction before modifying balance_kopeks.
Eagerly loads key relationships to avoid MissingGreenlet in async context.
"""
result = await db.execute(
select(User).where(User.id == user.id).with_for_update().execution_options(populate_existing=True)
select(User)
.where(User.id == user.id)
.options(
selectinload(User.subscription),
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
selectinload(User.promo_group),
selectinload(User.referrer),
)
.with_for_update()
.execution_options(populate_existing=True)
)
return result.scalar_one()
@@ -434,8 +444,18 @@ async def add_user_balance(
) -> bool:
try:
# Lock the user row to prevent concurrent balance race conditions
# Eagerly load key relationships to avoid MissingGreenlet in async context
locked_result = await db.execute(
select(User).where(User.id == user.id).with_for_update().execution_options(populate_existing=True)
select(User)
.where(User.id == user.id)
.options(
selectinload(User.subscription),
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
selectinload(User.promo_group),
selectinload(User.referrer),
)
.with_for_update()
.execution_options(populate_existing=True)
)
user = locked_result.scalar_one()
@@ -534,8 +554,18 @@ async def subtract_user_balance(
)
# Lock the user row to prevent concurrent balance race conditions
# Eagerly load key relationships to avoid MissingGreenlet in async context
locked_result = await db.execute(
select(User).where(User.id == user.id).with_for_update().execution_options(populate_existing=True)
select(User)
.where(User.id == user.id)
.options(
selectinload(User.subscription),
selectinload(User.user_promo_groups).selectinload(UserPromoGroup.promo_group),
selectinload(User.promo_group),
selectinload(User.referrer),
)
.with_for_update()
.execution_options(populate_existing=True)
)
user = locked_result.scalar_one()
+3 -2
View File
@@ -274,8 +274,9 @@ class ChannelSubscriptionService:
)
return False # Fail-closed -- bot cannot verify membership
except TelegramBadRequest as e:
if 'user not found' in str(e).lower():
return False # User never interacted with bot in that context
err_msg = str(e).lower()
if 'user not found' in err_msg or 'participant_id_invalid' in err_msg:
return False # User never interacted with bot/channel
logger.error('Bad request checking channel', channel_id=channel_id, error=str(e))
return False # Fail-closed
except TelegramNetworkError:
+5 -2
View File
@@ -24,6 +24,8 @@ def format_referrer_info(user: User) -> str:
try:
# Проверяем, является ли referrer обычным объектом или InstrumentedList
# getattr default does NOT catch MissingGreenlet (not an AttributeError),
# so we wrap in try/except to handle lazy-load failures in async context.
referrer = getattr(user, 'referrer', None)
# Если referrer это InstrumentedList или None, то возвращаем информацию по ID
@@ -39,8 +41,9 @@ def format_referrer_info(user: User) -> str:
return f'ID {referrer_telegram_id or referred_by_id}'
except (AttributeError, TypeError):
# Если возникла ошибка при обращении к атрибутам, просто возвращаем ID
except Exception:
# MissingGreenlet is not a subclass of AttributeError/TypeError,
# so we must catch broadly to handle lazy-load failures in async context.
return f'ID {referred_by_id} (ошибка загрузки)'