Сортировка в таблицах и попытка приструнить генерацию превьюшек
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s

This commit is contained in:
2026-04-03 01:10:05 +03:00
parent cddbfeadde
commit b76ce4913f
16 changed files with 722 additions and 34 deletions

View File

View File

@@ -0,0 +1,162 @@
import multiprocessing
import os
from django.core.management.base import BaseCommand
from django.db import close_old_connections
from django.utils import timezone
def _run_one_task_preview(task_id: int, out_q: "multiprocessing.Queue") -> None:
"""Обрабатывает одну деталь в отдельном процессе.
Зачем отдельный процесс:
- некоторые DXF/рендер могут «залипать» (бесконечно долго обрабатываться);
- поток внутри веб/команды не спасает от GIL и зависаний библиотеки;
- процесс можно принудительно завершить по таймауту.
Результат кладём в очередь, чтобы родитель понял: ok/skip/error.
"""
try:
# В дочернем процессе нужно инициализировать Django, чтобы работать с ORM.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'core.settings')
import django
django.setup()
from shiftflow.models import ProductionTask
from shiftflow.views import _update_task_preview
task = ProductionTask.objects.get(pk=task_id)
ok = bool(_update_task_preview(task))
out_q.put(('ok', ok))
except Exception as e:
out_q.put(('err', str(e)))
class Command(BaseCommand):
help = "Пакетная регенерация превью DXF и габаритов по активным сделкам."
def add_arguments(self, parser):
parser.add_argument("job_id", type=int)
def handle(self, *args, **options):
job_id = int(options["job_id"])
close_old_connections()
from shiftflow.models import DxfPreviewJob, DxfPreviewSettings, ProductionTask
try:
job = DxfPreviewJob.objects.get(pk=job_id)
except DxfPreviewJob.DoesNotExist:
return
job.status = "running"
job.started_at = timezone.now()
job.finished_at = None
job.last_message = ""
job.save(update_fields=["status", "started_at", "finished_at", "last_message"])
# Берём настройки таймаута из БД.
settings, _ = DxfPreviewSettings.objects.get_or_create(pk=1)
per_task_timeout = int(getattr(settings, 'per_task_timeout_sec', 45) or 45)
deal_statuses = ["lead", "work"]
qs = ProductionTask.objects.select_related("deal").filter(deal__status__in=deal_statuses)
total = qs.count()
DxfPreviewJob.objects.filter(pk=job_id).update(
total=total,
processed=0,
updated=0,
skipped=0,
errors=0,
)
processed = 0
updated = 0
skipped = 0
errors = 0
# Таймаут обработки одной детали (сек).
# Если конкретный DXF «залип» — задача не должна блокироваться навсегда.
per_task_timeout = 45
try:
for task in qs.iterator(chunk_size=50):
processed += 1
# Пишем “живой” статус до тяжёлой операции, чтобы UI видел движение.
DxfPreviewJob.objects.filter(pk=job_id).update(
processed=processed,
updated=updated,
skipped=skipped,
errors=errors,
last_message=f"Обработка {processed}/{total}: {task.drawing_name} (сделка {task.deal.number})",
)
# Поддержка мягкой отмены: админ нажал «Прервать», выходим после текущей детали.
if DxfPreviewJob.objects.filter(pk=job_id, cancel_requested=True).exists():
DxfPreviewJob.objects.filter(pk=job_id).update(
status='cancelled',
finished_at=timezone.now(),
last_message='Задача остановлена пользователем.',
)
return
# Обрабатываем одну деталь в отдельном процессе и ждём не больше per_task_timeout.
close_old_connections()
q: multiprocessing.Queue = multiprocessing.Queue(maxsize=1)
p = multiprocessing.Process(target=_run_one_task_preview, args=(task.id, q))
p.start()
p.join(per_task_timeout)
if p.is_alive():
# DXF/рендер завис — убиваем процесс и учитываем как ошибку.
p.terminate()
p.join(5)
errors += 1
DxfPreviewJob.objects.filter(pk=job_id).update(
processed=processed,
updated=updated,
skipped=skipped,
errors=errors,
last_message=f"Таймаут {per_task_timeout}с: {task.drawing_name} (сделка {task.deal.number})",
)
else:
try:
status, payload = q.get_nowait()
except Exception:
status, payload = ('err', 'no_result')
if status == 'ok':
if payload:
updated += 1
else:
skipped += 1
else:
errors += 1
close_old_connections()
DxfPreviewJob.objects.filter(pk=job_id).update(
processed=processed,
updated=updated,
skipped=skipped,
errors=errors,
)
DxfPreviewJob.objects.filter(pk=job_id).update(
status="done",
finished_at=timezone.now(),
last_message=f"Готово. Обновлены: {updated}. Пропущено: {skipped}. Ошибок: {errors}.",
)
except Exception:
DxfPreviewJob.objects.filter(pk=job_id).update(
status="failed",
finished_at=timezone.now(),
last_message="Задача завершилась с ошибкой (см. логи процесса).",
)
finally:
close_old_connections()