diff --git a/services/api/app/api/admin.py b/services/api/app/api/admin.py index d9f47e2..d3273c9 100644 --- a/services/api/app/api/admin.py +++ b/services/api/app/api/admin.py @@ -9,7 +9,7 @@ import io import logging import os import uuid -from datetime import UTC, datetime, timedelta +from datetime import datetime, timedelta from pathlib import Path import httpx @@ -929,20 +929,24 @@ async def debug_snapshot(db: AsyncSession = Depends(get_db)) -> dict: # Пользователь видит вечное «обрабатывается» — молча и без следов в логах, # поэтому такие задачи должны быть видны в отладке (нашлись экземпляры, # висевшие с мая). - # tasks.created_at/updated_at — timestamptz, поэтому сравниваем с - # осведомлённым о зоне временем (naive utcnow() дал бы TypeError) - task_stale_cutoff = datetime.now(UTC) - timedelta(hours=2) + # + # Возраст считает сама база: колонки в PostgreSQL — timestamptz, а модель + # объявляет их без зоны, и передача сюда Python-времени ломается на + # несовпадении (asyncpg отвергает aware-параметр, naive даёт неверный сдвиг). stale_tasks = ( await db.execute( - select(Task) - .where( - Task.status == "processing", - func.coalesce(Task.updated_at, Task.created_at) < task_stale_cutoff, - ) - .order_by(Task.created_at.desc()) - .limit(20) + text(""" + SELECT public_id, type, created_at, + round(extract(epoch FROM now() - coalesce(updated_at, created_at)) + / 3600.0, 1) AS stuck_hours + FROM tasks + WHERE status = 'processing' + AND coalesce(updated_at, created_at) < now() - interval '2 hours' + ORDER BY created_at DESC + LIMIT 20 + """) ) - ).scalars().all() + ).all() return { "generated_at": datetime.utcnow().isoformat(timespec="seconds"), @@ -980,9 +984,7 @@ async def debug_snapshot(db: AsyncSession = Depends(get_db)) -> dict: "public_id": t.public_id, "type": t.type, "created_at": t.created_at, - "stuck_hours": round( - (datetime.now(UTC) - (t.updated_at or t.created_at)).total_seconds() / 3600, 1 - ), + "stuck_hours": float(t.stuck_hours), } for t in stale_tasks ],