From 39951d6a0fdcd5459e1baa0a39376c082186a224 Mon Sep 17 00:00:00 2001 From: jze9 Date: Sat, 5 Sep 2026 19:13:01 +0500 Subject: [PATCH] =?UTF-8?q?feat(admin):=20=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D1=82=D1=8C=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D1=88=D0=B8=D0=B5=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA?= =?UTF-8?q?=D0=B8=20=D0=B2=20=D0=BF=D0=B0=D0=BD=D0=B5=D0=BB=D0=B8=20=D0=BE?= =?UTF-8?q?=D1=82=D0=BB=D0=B0=D0=B4=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Нашлись 4 задачи в статусе processing, висящие с 30 мая: пользователь видит вечное «обрабатывается», а в системе никаких следов. Теперь задачи без движения дольше 2 часов попадают в /admin/debug рядом с зависшими прогонами заливки, с указанием, сколько часов они стоят. tasks.created_at/updated_at — timestamptz, поэтому сравнение идёт с осведомлённым о зоне временем: naive utcnow() дал бы TypeError в рантайме. Co-Authored-By: Claude Opus 5 --- services/api/app/api/admin.py | 32 +++++++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/services/api/app/api/admin.py b/services/api/app/api/admin.py index 4897c3c..d9f47e2 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 datetime, timedelta +from datetime import UTC, datetime, timedelta from pathlib import Path import httpx @@ -925,6 +925,25 @@ async def debug_snapshot(db: AsyncSession = Depends(get_db)) -> dict: ) ).scalars().all() + # Зависшая проверка: числится в работе, но воркер давно её не трогал. + # Пользователь видит вечное «обрабатывается» — молча и без следов в логах, + # поэтому такие задачи должны быть видны в отладке (нашлись экземпляры, + # висевшие с мая). + # tasks.created_at/updated_at — timestamptz, поэтому сравниваем с + # осведомлённым о зоне временем (naive utcnow() дал бы TypeError) + task_stale_cutoff = datetime.now(UTC) - timedelta(hours=2) + 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) + ) + ).scalars().all() + return { "generated_at": datetime.utcnow().isoformat(timespec="seconds"), "celery": celery_state, @@ -956,6 +975,17 @@ async def debug_snapshot(db: AsyncSession = Depends(get_db)) -> dict: } for t in failed_tasks ], + "stale_tasks": [ + { + "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 + ), + } + for t in stale_tasks + ], # Ключи бэкендов задаются в .env (генерируется из Infisical) и влияют на # поведение воркеров — при разборе «почему не так считает» нужны первыми "config": {