feat(admin): показывать зависшие проверки в панели отладки
All checks were successful
Deploy / test (push) Successful in 3m44s
Deploy / deploy (push) Successful in 8m39s

Нашлись 4 задачи в статусе processing, висящие с 30 мая: пользователь видит
вечное «обрабатывается», а в системе никаких следов. Теперь задачи без
движения дольше 2 часов попадают в /admin/debug рядом с зависшими прогонами
заливки, с указанием, сколько часов они стоят.

tasks.created_at/updated_at — timestamptz, поэтому сравнение идёт с
осведомлённым о зоне временем: naive utcnow() дал бы TypeError в рантайме.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
jze9
2026-09-05 19:13:01 +05:00
parent f4092e5331
commit 39951d6a0f

View File

@@ -9,7 +9,7 @@ import io
import logging import logging
import os import os
import uuid import uuid
from datetime import datetime, timedelta from datetime import UTC, datetime, timedelta
from pathlib import Path from pathlib import Path
import httpx import httpx
@@ -925,6 +925,25 @@ async def debug_snapshot(db: AsyncSession = Depends(get_db)) -> dict:
) )
).scalars().all() ).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 { return {
"generated_at": datetime.utcnow().isoformat(timespec="seconds"), "generated_at": datetime.utcnow().isoformat(timespec="seconds"),
"celery": celery_state, "celery": celery_state,
@@ -956,6 +975,17 @@ async def debug_snapshot(db: AsyncSession = Depends(get_db)) -> dict:
} }
for t in failed_tasks 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) и влияют на # Ключи бэкендов задаются в .env (генерируется из Infisical) и влияют на
# поведение воркеров — при разборе «почему не так считает» нужны первыми # поведение воркеров — при разборе «почему не так считает» нужны первыми
"config": { "config": {