Сортировка в таблицах и попытка приструнить генерацию превьюшек
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
All checks were successful
Deploy MES Core / deploy (push) Successful in 13s
This commit is contained in:
0
shiftflow/management/__init__.py
Normal file
0
shiftflow/management/__init__.py
Normal file
0
shiftflow/management/commands/__init__.py
Normal file
0
shiftflow/management/commands/__init__.py
Normal file
162
shiftflow/management/commands/dxf_preview_job.py
Normal file
162
shiftflow/management/commands/dxf_preview_job.py
Normal 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()
|
||||
Reference in New Issue
Block a user