Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 22 additions & 16 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
from src.presentation.middlewares import MetricsMiddleware
from src.infrastructure.tasks.cleanup_magic_tokens import cleanup_telegram_tokens
from src.config import get_database_url
from src.infrastructure.tasks.remove_bans import remove_expired_bans
from src.infrastructure.db import database

scheduler = AsyncIOScheduler() # Планировщик

@asynccontextmanager
async def lifespan(app: FastAPI):
Comment on lines +21 to 24

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Используйте поддерживаемый API остановки планировщика вместо dispose() и обеспечьте корректное управление его жизненным циклом.

В APScheduler 3.x у AsyncIOScheduler нет метода dispose(), поэтому при завершении работы будет выброшен AttributeError. Вместо этого используйте scheduler.shutdown(wait=False) (или wait=True, если нужно дождаться завершения задач). Поскольку планировщик является синглтоном на уровне модуля, также важно гарантировать, что его остановка идемпотентна и что start()/shutdown() не могут быть вызваны в неверном порядке (например, в тестах или при перезапуске приложения).

Original comment in English

issue (bug_risk): Use the scheduler’s supported shutdown API instead of dispose() and ensure proper lifecycle handling.

AsyncIOScheduler in APScheduler 3.x doesn’t have dispose(), so this will raise AttributeError on teardown. Use scheduler.shutdown(wait=False) (or wait=True if needed) instead. Since the scheduler is a module-level singleton, also ensure shutdown is idempotent and that start()/shutdown() can’t be called in an inconsistent order (e.g., across tests or reloads).

Expand All @@ -24,17 +28,31 @@ async def lifespan(app: FastAPI):
DB_URL
)

SessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
database.SessionLocal = async_sessionmaker(
bind=engine,
class_=AsyncSession,
expire_on_commit=False
)

app.state.SessionLocal = SessionLocal
@app.on_event("startup")
async def startup():
scheduler.add_job(
cleanup_telegram_tokens,
"interval",
minutes=5
)
scheduler.add_job(
remove_expired_bans,
"interval",
minutes=10
)
scheduler.start()

# При старте выполняется ко до yiled
yield
# выполняется код после yiled и остановка

scheduler.dispose()
await engine.dispose()


Expand Down Expand Up @@ -69,18 +87,6 @@ async def lifespan(app: FastAPI):
allow_credentials=True
)

scheduler = AsyncIOScheduler() # Планировщик
@app.on_event("startup")
async def startup():
scheduler.add_job(
cleanup_telegram_tokens,
"interval",
minutes=5
)
scheduler.start()



@app.get("/")
@limiter.limit("5/minute")
def welcome(request: Request):
Expand Down
3 changes: 1 addition & 2 deletions src/domain/models/ban_model.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from datetime import datetime, timezone
from typing import TYPE_CHECKING

from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, Integer, String, func
from sqlalchemy import CheckConstraint, DateTime, ForeignKey, Index, Integer, String
from sqlalchemy.orm import Mapped, mapped_column, relationship, validates

from src.infrastructure.db.database import Base
Expand Down Expand Up @@ -56,7 +56,6 @@ class Ban(Base):
unique=True,
postgresql_where=(
revoked_at.is_(None)
& (expires_at.is_(None) | (expires_at > func.now()))
),
),
)
Expand Down
17 changes: 10 additions & 7 deletions src/infrastructure/db/database.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
from sqlalchemy.orm import declarative_base
from fastapi import Request
from sqlalchemy.ext.asyncio import (
AsyncSession,
async_sessionmaker
)

Base = declarative_base()

async def get_db(request: Request):

async with request.app.state.SessionLocal() as session:
try:
SessionLocal: async_sessionmaker[AsyncSession] | None = None

async def get_db():
if SessionLocal is None:
raise RuntimeError("Database is not initialied")
async with SessionLocal() as session:
yield session
finally:
await session.close()



2 changes: 1 addition & 1 deletion src/infrastructure/tasks/cleanup_magic_tokens.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from sqlalchemy import delete, or_
from datetime import datetime, timezone

from src.infrastructure.db.database import SessionLocal
from src.domain.models import MagicToken
from src.logger import logger
from src.infrastructure.db.database import SessionLocal

async def cleanup_telegram_tokens():
"""Очистка истекших токенов"""
Expand Down
19 changes: 19 additions & 0 deletions src/infrastructure/tasks/remove_bans.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sqlalchemy import update
from datetime import datetime, timezone

from src.domain.models.ban_model import Ban
from src.infrastructure.db.database import SessionLocal

async def remove_expired_bans():
async with SessionLocal() as db:
await db.execute(update(Ban).where(
Ban.expires_at.is_not(None),
Ban.expires_at < datetime.now(timezone.utc),
Ban.revoked_at.is_(None)
).values(
revoked_at = datetime.now(timezone.utc),
revoked_reason="Expiration of the Term"
)
)

await db.commit()
Loading