From 7fbb64ed8f5e1593265a3dde81b78d7b3d5c9a59 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 6 May 2026 23:38:51 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=20Bolt:=20Prevent=20event=20loop=20st?= =?UTF-8?q?arvation=20in=20API=20endpoints?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Offloads blocking synchronous operations (PDF generation via subprocess, ATS regex compilation, AI customization API requests) inside FastAPI async route handlers to background worker threads using `anyio.to_thread.run_sync`. * Utilizes `functools.partial` to safely pass kwargs to target functions. * Prevents main thread starvation and dramatically improves application throughput under concurrent loads. * Added corresponding Bolt journal entry for ASGI event loop starvation optimizations. Co-authored-by: anchapin <6326294+anchapin@users.noreply.github.com> --- .jules/bolt.md | 4 ++++ api/main.py | 31 +++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.jules/bolt.md b/.jules/bolt.md index 254b8d5..6eca001 100644 --- a/.jules/bolt.md +++ b/.jules/bolt.md @@ -13,3 +13,7 @@ ## 2025-02-18 - Regex Pre-compilation in Hot Paths **Learning:** Re-compiling regexes inside a frequently called function (like `latex_escape` which runs for every string) creates significant overhead. Pre-compiling them at module level yielded a ~3.2x speedup. **Action:** Always look for regex compilations inside loops or frequently called functions and move them to module level constants. + +## 2025-02-18 - API Event Loop Starvation +**Learning:** In ASGI applications like FastAPI, running heavy synchronous tasks (like PDF compilation via `pdflatex`, running large ATS regex generation, or making blocking external AI calls) blocks the main thread, leading to event loop starvation and reducing throughput dramatically. +**Action:** Always wrap heavy synchronous functions using `functools.partial` and offload them to background worker threads using `await anyio.to_thread.run_sync()`. diff --git a/api/main.py b/api/main.py index 08b7b6c..9bc1182 100644 --- a/api/main.py +++ b/api/main.py @@ -1,8 +1,10 @@ +import functools import logging import os import tempfile from pathlib import Path +import anyio import yaml from fastapi import FastAPI, HTTPException, Response, Security from fastapi.middleware.cors import CORSMiddleware @@ -171,7 +173,13 @@ async def render_pdf(request: ResumeRequest): # We output to a temp file output_pdf = temp_path / "output.pdf" - generator.generate(variant=request.variant, output_format="pdf", output_path=output_pdf) + func = functools.partial( + generator.generate, + variant=request.variant, + output_format="pdf", + output_path=output_pdf, + ) + await anyio.to_thread.run_sync(func) if not output_pdf.exists(): raise HTTPException(status_code=500, detail="PDF generation failed") @@ -215,9 +223,12 @@ async def tailor_resume(request: TailorRequest): # We pass None for yaml_path as we use direct data generator = AIGenerator(yaml_path=None, config=config) - tailored_data = generator.tailor_data( - resume_data=request.resume_data, job_description=request.job_description + func = functools.partial( + generator.tailor_data, + resume_data=request.resume_data, + job_description=request.job_description, ) + tailored_data = await anyio.to_thread.run_sync(func) return tailored_data @@ -248,7 +259,10 @@ async def ats_check(request: ATSRequest): # Generate ATS report directly from resume data generator = ATSGenerator(config=config, resume_data=request.resume_data) - report = generator.generate_report(request.job_description, request.variant) + func = functools.partial( + generator.generate_report, request.job_description, request.variant + ) + report = await anyio.to_thread.run_sync(func) # Convert to JSON-serializable format result = { @@ -300,12 +314,14 @@ async def generate_cover_letter(request: CoverLetterRequest): # Generate cover letter (always use non-interactive for API) # The provided answers will be used as context by the AI - outputs, job_details = generator.generate_non_interactive( + func = functools.partial( + generator.generate_non_interactive, job_description=request.job_description, company_name=request.company_name, variant=request.variant, output_formats=[request.format], ) + outputs, job_details = await anyio.to_thread.run_sync(func) # Return the generated content # Note: outputs["md"] contains the rendered markdown, outputs["pdf"] contains LaTeX @@ -560,7 +576,10 @@ async def render_resume_pdf(resume_id: str, variant: str = "base"): generator = TemplateGenerator(yaml_path=resume_yaml_path) output_pdf = temp_path / "output.pdf" - generator.generate(variant=variant, output_format="pdf", output_path=output_pdf) + func = functools.partial( + generator.generate, variant=variant, output_format="pdf", output_path=output_pdf + ) + await anyio.to_thread.run_sync(func) if not output_pdf.exists(): raise HTTPException(status_code=500, detail="PDF generation failed")