fix: reduce memory usage for monthly ZIP archive flow
Replace generated archive download response with FileResponse streaming so ZIP files are not fully loaded into process memory. Move monthly archive task output to a temporary file and upload that file to storage, and write invoice attachments into ZIP archives in chunks. Add Celery memory guard settings, a memory-aware base task hook, and periodic /tmp cleanup for prefixed archive files older than one day. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -5,6 +5,7 @@ from django.core.mail import EmailMessage
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.core.files.base import ContentFile
|
||||
from django.core.files import File
|
||||
import logging
|
||||
from io import BytesIO
|
||||
from io import StringIO
|
||||
@@ -13,6 +14,9 @@ import base64
|
||||
import re
|
||||
import csv
|
||||
import mimetypes
|
||||
import os
|
||||
import tempfile
|
||||
import zipfile
|
||||
from decimal import Decimal, InvalidOperation
|
||||
from smtplib import SMTPSenderRefused
|
||||
from datetime import timedelta
|
||||
@@ -838,10 +842,9 @@ def process_work_order_invoice_ocr(invoice_id):
|
||||
@shared_task
|
||||
def build_monthly_archive_cached_task(generated_archive_id):
|
||||
from .models import GeneratedFleetArchive
|
||||
from .services import NotificationService
|
||||
from .views import (
|
||||
_build_monthly_service_tasks_archive_content,
|
||||
_build_monthly_work_orders_archive_content,
|
||||
_write_monthly_service_tasks_archive_entries,
|
||||
_write_monthly_work_orders_archive_entries,
|
||||
_notify_monthly_archive_request,
|
||||
)
|
||||
|
||||
@@ -854,25 +857,39 @@ def build_monthly_archive_cached_task(generated_archive_id):
|
||||
if generated is None:
|
||||
return {"status": "failed", "error": "Generated archive record not found"}
|
||||
|
||||
tmp_dir = str(getattr(settings, 'APP_TMP_CLEANUP_DIR', '/tmp'))
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode='w+b',
|
||||
delete=False,
|
||||
dir=tmp_dir if os.path.isdir(tmp_dir) else None,
|
||||
prefix='erp-fleet-archive-',
|
||||
suffix='.zip',
|
||||
) as temp_zip:
|
||||
temp_zip_path = temp_zip.name
|
||||
try:
|
||||
if generated.archive_type == 'service_tasks':
|
||||
archive_content = _build_monthly_service_tasks_archive_content(
|
||||
user=generated.requested_by,
|
||||
year=generated.year,
|
||||
month=generated.month,
|
||||
)
|
||||
else:
|
||||
archive_content = _build_monthly_work_orders_archive_content(
|
||||
user=generated.requested_by,
|
||||
year=generated.year,
|
||||
month=generated.month,
|
||||
)
|
||||
|
||||
if not archive_content:
|
||||
with zipfile.ZipFile(temp_zip_path, mode='w', compression=zipfile.ZIP_DEFLATED) as archive:
|
||||
if generated.archive_type == 'service_tasks':
|
||||
entries_written = _write_monthly_service_tasks_archive_entries(
|
||||
archive,
|
||||
user=generated.requested_by,
|
||||
year=generated.year,
|
||||
month=generated.month,
|
||||
)
|
||||
else:
|
||||
entries_written = _write_monthly_work_orders_archive_entries(
|
||||
archive,
|
||||
user=generated.requested_by,
|
||||
year=generated.year,
|
||||
month=generated.month,
|
||||
)
|
||||
if entries_written == 0:
|
||||
raise ValueError('ZIP arhiva je prazna.')
|
||||
|
||||
if os.path.getsize(temp_zip_path) <= 0:
|
||||
raise ValueError('ZIP arhiva je prazna.')
|
||||
filename = generated.filename or f"{generated.archive_type}-{generated.year}-{generated.month}.zip"
|
||||
generated.file.save(filename, ContentFile(archive_content), save=False)
|
||||
with open(temp_zip_path, 'rb') as temp_file:
|
||||
generated.file.save(filename, File(temp_file), save=False)
|
||||
generated.status = 'ready'
|
||||
generated.error_message = ''
|
||||
generated.save(update_fields=['file', 'status', 'error_message', 'updated_at'])
|
||||
@@ -902,6 +919,12 @@ def build_monthly_archive_cached_task(generated_archive_id):
|
||||
)
|
||||
logger.exception("Greška kod build_monthly_archive_cached_task: %s", exc)
|
||||
return {"status": "failed", "error": str(exc)}
|
||||
finally:
|
||||
try:
|
||||
if os.path.exists(temp_zip_path):
|
||||
os.remove(temp_zip_path)
|
||||
except OSError:
|
||||
logger.warning("Ne mogu obrisati privremenu ZIP datoteku: %s", temp_zip_path)
|
||||
|
||||
|
||||
@shared_task
|
||||
@@ -944,6 +967,46 @@ def cleanup_expired_generated_pdfs_task():
|
||||
return {"deleted": deleted}
|
||||
|
||||
|
||||
@shared_task
|
||||
def cleanup_tmp_files_task():
|
||||
tmp_dir = str(getattr(settings, 'APP_TMP_CLEANUP_DIR', '/tmp'))
|
||||
max_age_hours = int(getattr(settings, 'APP_TMP_CLEANUP_MAX_AGE_HOURS', 24))
|
||||
prefixes = tuple(getattr(settings, 'APP_TMP_CLEANUP_PREFIXES', ['erp-fleet-archive-']))
|
||||
|
||||
if max_age_hours <= 0:
|
||||
raise ValueError('APP_TMP_CLEANUP_MAX_AGE_HOURS mora biti > 0.')
|
||||
if not prefixes:
|
||||
raise ValueError('APP_TMP_CLEANUP_PREFIXES ne smije biti prazan.')
|
||||
if not os.path.isdir(tmp_dir):
|
||||
return {'deleted': 0, 'tmp_dir': tmp_dir, 'reason': 'tmp-dir-not-found'}
|
||||
|
||||
now = timezone.now().timestamp()
|
||||
cutoff = now - (max_age_hours * 3600)
|
||||
deleted = 0
|
||||
|
||||
for name in os.listdir(tmp_dir):
|
||||
if not any(name.startswith(prefix) for prefix in prefixes):
|
||||
continue
|
||||
full_path = os.path.join(tmp_dir, name)
|
||||
try:
|
||||
stat_info = os.stat(full_path)
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
if stat_info.st_mtime > cutoff:
|
||||
continue
|
||||
if os.path.isdir(full_path):
|
||||
try:
|
||||
os.rmdir(full_path)
|
||||
except OSError:
|
||||
logger.warning("Preskačem %s: direktorij nije prazan ili je nedostupan.", full_path)
|
||||
continue
|
||||
else:
|
||||
os.remove(full_path)
|
||||
deleted += 1
|
||||
|
||||
return {'deleted': deleted, 'tmp_dir': tmp_dir, 'max_age_hours': max_age_hours}
|
||||
|
||||
|
||||
@shared_task
|
||||
def build_work_order_pdf_cached_task(generated_pdf_id):
|
||||
from .views import _build_work_order_pdf, _build_work_order_service_records_pdf
|
||||
|
||||
Reference in New Issue
Block a user