fix(async): chiamate bloccanti in asyncio.to_thread (#36 punto 1)
kaleido (pio.to_image), WeasyPrint (write_pdf), pdfplumber (extract_text_from_pdf) e la scrittura del CSV di produzione giravano sincrone dentro handler async: un PDF pesante o un run grande fermava il worker per tutte le altre richieste. Ora avvolte in await asyncio.to_thread(...) in report_service.py, ai_service.py e production_export_service.py. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
"""AI service for parsing technical sheets via OpenRouter."""
|
"""AI service for parsing technical sheets via OpenRouter."""
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
@@ -36,7 +37,9 @@ def extract_text_from_pdf(pdf_bytes: bytes) -> str:
|
|||||||
|
|
||||||
|
|
||||||
async def parse_technical_sheet(pdf_bytes: bytes) -> list[dict]:
|
async def parse_technical_sheet(pdf_bytes: bytes) -> list[dict]:
|
||||||
text = extract_text_from_pdf(pdf_bytes)
|
# pdfplumber parses synchronously and can take a while on a big PDF: off
|
||||||
|
# the event loop, or one upload stalls every other request being served.
|
||||||
|
text = await asyncio.to_thread(extract_text_from_pdf, pdf_bytes)
|
||||||
if not text.strip():
|
if not text.strip():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ The CSV uses the delimiters configured in system_settings, the same ones the man
|
|||||||
export honours, so a shop floor set to the Italian convention (';' and ',') gets
|
export honours, so a shop floor set to the Italian convention (';' and ',') gets
|
||||||
files that open correctly in their Excel.
|
files that open correctly in their Excel.
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
import logging
|
import logging
|
||||||
@@ -192,7 +193,11 @@ async def export_run_statistics(db: AsyncSession, run: ProductionRun) -> str | N
|
|||||||
|
|
||||||
stamp = (run.closed_at or datetime.now()).strftime("%Y%m%d_%H%M%S")
|
stamp = (run.closed_at or datetime.now()).strftime("%Y%m%d_%H%M%S")
|
||||||
filename = f"production_{run.id}_{stamp}.csv"
|
filename = f"production_{run.id}_{stamp}.csv"
|
||||||
(directory / filename).write_text(content, encoding="utf-8-sig")
|
# A large run's file can take a moment to write: off the event loop, so
|
||||||
|
# closing one run does not stall every other request being served.
|
||||||
|
await asyncio.to_thread(
|
||||||
|
(directory / filename).write_text, content, encoding="utf-8-sig"
|
||||||
|
)
|
||||||
|
|
||||||
# utf-8-sig: the BOM is what makes Excel read accented characters correctly,
|
# utf-8-sig: the BOM is what makes Excel read accented characters correctly,
|
||||||
# and these files are opened in Excel on the shop floor.
|
# and these files are opened in Excel on the shop floor.
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
"""Report generation service — PDF reports via Jinja2 + Plotly/Kaleido + WeasyPrint."""
|
"""Report generation service — PDF reports via Jinja2 + Plotly/Kaleido + WeasyPrint."""
|
||||||
|
import asyncio
|
||||||
import base64
|
import base64
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -204,6 +205,11 @@ def _generate_control_chart_svg(chart_data, subtask_info: dict | None) -> str:
|
|||||||
return svg_bytes.decode("utf-8")
|
return svg_bytes.decode("utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def _render_pdf(html_content: str) -> bytes:
|
||||||
|
"""Blocking WeasyPrint render — always call via asyncio.to_thread."""
|
||||||
|
return HTML(string=html_content).write_pdf()
|
||||||
|
|
||||||
|
|
||||||
def _generate_histogram_svg(hist_data, subtask_info: dict | None) -> str:
|
def _generate_histogram_svg(hist_data, subtask_info: dict | None) -> str:
|
||||||
"""Generate histogram SVG using Plotly + Kaleido."""
|
"""Generate histogram SVG using Plotly + Kaleido."""
|
||||||
if not hist_data.bins:
|
if not hist_data.bins:
|
||||||
@@ -300,9 +306,13 @@ async def generate_spc_report(
|
|||||||
)
|
)
|
||||||
histogram = compute_histogram(values)
|
histogram = compute_histogram(values)
|
||||||
|
|
||||||
# 3. Generate SVG charts
|
# 3. Generate SVG charts (kaleido shells out and blocks: off the event loop)
|
||||||
control_chart_svg = _generate_control_chart_svg(control_chart, subtask_info)
|
control_chart_svg = await asyncio.to_thread(
|
||||||
histogram_svg = _generate_histogram_svg(histogram, subtask_info)
|
_generate_control_chart_svg, control_chart, subtask_info
|
||||||
|
)
|
||||||
|
histogram_svg = await asyncio.to_thread(
|
||||||
|
_generate_histogram_svg, histogram, subtask_info
|
||||||
|
)
|
||||||
|
|
||||||
# 4. Get company info and recipe info
|
# 4. Get company info and recipe info
|
||||||
company = await _get_company_info(db)
|
company = await _get_company_info(db)
|
||||||
@@ -336,8 +346,8 @@ async def generate_spc_report(
|
|||||||
n_measurements=len(measurements),
|
n_measurements=len(measurements),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 7. Convert HTML to PDF
|
# 7. Convert HTML to PDF (WeasyPrint blocks: off the event loop)
|
||||||
pdf_bytes = HTML(string=html_content).write_pdf()
|
pdf_bytes = await asyncio.to_thread(_render_pdf, html_content)
|
||||||
return pdf_bytes
|
return pdf_bytes
|
||||||
|
|
||||||
|
|
||||||
@@ -424,6 +434,6 @@ async def generate_measurement_report(
|
|||||||
n_measurements=len(measurements),
|
n_measurements=len(measurements),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 8. Convert to PDF
|
# 8. Convert to PDF (WeasyPrint blocks: off the event loop)
|
||||||
pdf_bytes = HTML(string=html_content).write_pdf()
|
pdf_bytes = await asyncio.to_thread(_render_pdf, html_content)
|
||||||
return pdf_bytes
|
return pdf_bytes
|
||||||
|
|||||||
Reference in New Issue
Block a user