fix: BILINEAR+BaseException catch to fix image worker timeout in PDF/DOCX
Root cause was WORKER TIMEOUT, not OOM: - Gunicorn sends SIGABRT to workers that exceed 60s timeout - SIGABRT handler calls sys.exit(1) raising SystemExit(BaseException) - except Exception does NOT catch SystemExit, so the worker crashes Two fixes: 1. except BaseException — catches SystemExit so the worker survives and gracefully skips the image (returns None) instead of dying 2. Image.BILINEAR instead of LANCZOS — orders of magnitude faster for large images, ensures processing completes well within 60s 3. Reduce max_width 1600->800, megapixel limit 20->8 MP, optimize=True removed (these reduce processing time further) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -464,32 +464,31 @@ def _resolve_service_report_tasks(work_order, task_id):
|
|||||||
return [selected_task], selected_task
|
return [selected_task], selected_task
|
||||||
|
|
||||||
|
|
||||||
_COMPRESS_IMAGE_MAX_MEGAPIXELS = 20 # preskači slike iznad 20 MP kako bi se izbjegao OOM
|
_COMPRESS_IMAGE_MAX_MEGAPIXELS = 8 # preskači slike iznad 8 MP; sprječava timeout i OOM
|
||||||
|
|
||||||
|
|
||||||
def _compress_image_for_pdf(image_field, max_width=1280, quality=75):
|
def _compress_image_for_pdf(image_field, max_width=800, quality=75):
|
||||||
"""
|
"""
|
||||||
Otvori image_field (Django FileField), kompresiraj na max_width JPEG u memoriji,
|
Otvori image_field (Django FileField), kompresiraj na max_width JPEG u memoriji,
|
||||||
vrati ImageReader spreman za reportlab. Vraća None ako slika nije dostupna.
|
vrati ImageReader spreman za reportlab. Vraća None ako slika nije dostupna.
|
||||||
|
|
||||||
Redoslijed thumbnail() → convert() je bitan: thumbnail() interno poziva draft()
|
BILINEAR umjesto LANCZOS: višestruko brže za velike slike (izbjeći Gunicorn timeout).
|
||||||
za JPEG (smanjuje rezoluciju JPEG dekodera na serveru) te radi in-place resize
|
Megapixel guard čita samo header i preskače slike > 8 MP bez dekodiranja piksela.
|
||||||
bez alokacije full-resolution kopije. convert() se poziva tek na već maloj slici.
|
except BaseException hvata i SystemExit koji Gunicorn diže na SIGABRT (worker timeout).
|
||||||
Pixel-count guard preskači slike koje bi i uz draft() prekoračile RAM.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
image_field.open('rb')
|
image_field.open('rb')
|
||||||
with Image.open(image_field) as img:
|
with Image.open(image_field) as img:
|
||||||
w, h = img.size # čita samo header, bez dekodiranja piksela
|
w, h = img.size
|
||||||
if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000:
|
if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000:
|
||||||
return None
|
return None
|
||||||
img.thumbnail((max_width, max_width * 4), Image.LANCZOS)
|
img.thumbnail((max_width, max_width * 2), Image.BILINEAR)
|
||||||
rgb = img.convert('RGB')
|
rgb = img.convert('RGB')
|
||||||
buf = BytesIO()
|
buf = BytesIO()
|
||||||
rgb.save(buf, format='JPEG', quality=quality, optimize=True)
|
rgb.save(buf, format='JPEG', quality=quality)
|
||||||
buf.seek(0)
|
buf.seek(0)
|
||||||
return ImageReader(buf)
|
return ImageReader(buf)
|
||||||
except Exception:
|
except BaseException:
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
@@ -498,28 +497,27 @@ def _compress_image_for_pdf(image_field, max_width=1280, quality=75):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _compress_image_for_docx(image_field, max_width=1600, quality=80):
|
def _compress_image_for_docx(image_field, max_width=800, quality=80):
|
||||||
"""
|
"""
|
||||||
Pripremi sliku za python-docx kao JPEG stream razumne veličine.
|
Pripremi sliku za python-docx kao JPEG stream razumne veličine.
|
||||||
|
|
||||||
Redoslijed thumbnail() → convert() je bitan: thumbnail() interno poziva draft()
|
BILINEAR umjesto LANCZOS: višestruko brže za velike slike (izbjeći Gunicorn timeout).
|
||||||
za JPEG (smanjuje rezoluciju JPEG dekodera na serveru) te radi in-place resize
|
Megapixel guard čita samo header i preskače slike > 8 MP bez dekodiranja piksela.
|
||||||
bez alokacije full-resolution kopije. convert() se poziva tek na već maloj slici.
|
except BaseException hvata i SystemExit koji Gunicorn diže na SIGABRT (worker timeout).
|
||||||
Pixel-count guard preskači slike koje bi i uz draft() prekoračile RAM.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
image_field.open('rb')
|
image_field.open('rb')
|
||||||
with Image.open(image_field) as img:
|
with Image.open(image_field) as img:
|
||||||
w, h = img.size # čita samo header, bez dekodiranja piksela
|
w, h = img.size
|
||||||
if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000:
|
if w * h > _COMPRESS_IMAGE_MAX_MEGAPIXELS * 1_000_000:
|
||||||
return None
|
return None
|
||||||
img.thumbnail((max_width, max_width * 4), Image.LANCZOS)
|
img.thumbnail((max_width, max_width * 2), Image.BILINEAR)
|
||||||
rgb = img.convert('RGB')
|
rgb = img.convert('RGB')
|
||||||
buf = BytesIO()
|
buf = BytesIO()
|
||||||
rgb.save(buf, format='JPEG', quality=quality, optimize=True)
|
rgb.save(buf, format='JPEG', quality=quality)
|
||||||
buf.seek(0)
|
buf.seek(0)
|
||||||
return buf
|
return buf
|
||||||
except Exception:
|
except BaseException:
|
||||||
return None
|
return None
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
|
|||||||
Reference in New Issue
Block a user