Fix blocking I/O operations in async route handlers

Wrapped blocking file I/O and CPU-bound operations with
asyncio.to_thread() to prevent blocking the event loop:

- about.py: stats() and cross_references_index() now compute
  in thread pool (extensive JSON loading and iteration)
- commentary.py: commentary_index() file I/O in thread pool
- misc.py: OG image fallback read_bytes() in thread pool

These routes perform heavy file I/O (reading 66+ JSON files,
iterating 31k verses) which would block all other requests
if run in the async context directly.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-12 13:11:40 -05:00
parent 5514218320
commit fd9e89f565
3 changed files with 60 additions and 31 deletions
+44 -25
View File
@@ -1,4 +1,5 @@
"""About routes - stats, cross-references index, and about page."""
import asyncio
import json
import re
from collections import defaultdict
@@ -22,12 +23,11 @@ def init_templates(t: Jinja2Templates):
# =============================================================================
# Routes
# Helper Functions (run in thread pool)
# =============================================================================
@router.get("/about/stats", response_class=HTMLResponse)
async def stats(request: Request):
"""Hidden statistics page - comprehensive site metrics"""
def _compute_stats() -> dict:
"""Compute all statistics - runs in thread pool to avoid blocking."""
data_dir = Path(__file__).parent.parent / "data"
# Bible statistics
@@ -148,7 +148,7 @@ async def stats(request: Request):
total_hebrew_entries = 0
total_greek_entries = 0
stats_data = {
return {
'bible': {
'total_verses': total_verses,
'total_books': total_books,
@@ -207,27 +207,9 @@ async def stats(request: Request):
}
}
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "About", "url": "/about"},
{"text": "Statistics", "url": None}
]
return templates.TemplateResponse(
"stats.html",
{
"request": request,
"books": books,
"stats": stats_data,
"breadcrumbs": breadcrumbs,
}
)
@router.get("/about/cross-references", response_class=HTMLResponse)
async def cross_references_index(request: Request):
"""Cross-references index - list all verses with cross-references"""
def _compute_crossref_index() -> tuple:
"""Compute cross-reference index - runs in thread pool."""
data_dir = Path(__file__).parent.parent / "data" / "cross_references"
# Build index of all verses with cross-references, grouped by book
@@ -273,6 +255,43 @@ async def cross_references_index(request: Request):
for verses in chapters.values()
)
return crossref_index, total_books, total_verses, total_refs
# =============================================================================
# Routes
# =============================================================================
@router.get("/about/stats", response_class=HTMLResponse)
async def stats(request: Request):
"""Hidden statistics page - comprehensive site metrics"""
# Run heavy computation in thread pool
stats_data = await asyncio.to_thread(_compute_stats)
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "About", "url": "/about"},
{"text": "Statistics", "url": None}
]
return templates.TemplateResponse(
"stats.html",
{
"request": request,
"books": books,
"stats": stats_data,
"breadcrumbs": breadcrumbs,
}
)
@router.get("/about/cross-references", response_class=HTMLResponse)
async def cross_references_index(request: Request):
"""Cross-references index - list all verses with cross-references"""
# Run heavy I/O in thread pool
crossref_index, total_books, total_verses, total_refs = await asyncio.to_thread(_compute_crossref_index)
books = bible.get_books()
breadcrumbs = [
{"text": "Home", "url": "/"},
+14 -5
View File
@@ -5,8 +5,10 @@ This module contains the commentary generation system including:
- Helper functions for generating theological commentary
- Book summaries, chapter overviews, and verse analysis
"""
import asyncio
import json
import random
from collections import defaultdict
from functools import lru_cache
from pathlib import Path
from fastapi import APIRouter, Request, HTTPException
@@ -20,10 +22,8 @@ router = APIRouter(tags=["Commentary"])
templates = None
@router.get("/about/commentary", response_class=HTMLResponse)
async def commentary_index(request: Request):
"""Commentary index - list all verses with commentary"""
from collections import defaultdict
def _compute_commentary_index() -> tuple:
"""Compute commentary index - runs in thread pool."""
from ..utils.books import OT_BOOKS, NT_BOOKS
data_dir = Path(__file__).parent.parent / "data" / "verse_commentary"
@@ -58,6 +58,15 @@ async def commentary_index(request: Request):
for verses in chapters.values()
)
return commentary_index, total_books, total_verses
@router.get("/about/commentary", response_class=HTMLResponse)
async def commentary_index(request: Request):
"""Commentary index - list all verses with commentary"""
# Run heavy I/O in thread pool
commentary_idx, total_books, total_verses = await asyncio.to_thread(_compute_commentary_index)
breadcrumbs = [
{"text": "Home", "url": "/"},
{"text": "About", "url": "/about"},
@@ -73,7 +82,7 @@ async def commentary_index(request: Request):
{
"request": request,
"books": books,
"commentary_index": commentary_index,
"commentary_index": commentary_idx,
"total_books": total_books,
"total_verses": total_verses,
"breadcrumbs": breadcrumbs,
+2 -1
View File
@@ -413,7 +413,8 @@ async def og_image_verse(
# Return default image if verse not found
from pathlib import Path as PathLib
default_path = PathLib(__file__).parent.parent / "static" / "og-image.png"
return Response(content=default_path.read_bytes(), media_type="image/png")
content = await asyncio.to_thread(default_path.read_bytes)
return Response(content=content, media_type="image/png")
title = f"{book} {chapter}:{verse}"
cache_key = f"verse:{book}:{chapter}:{verse}"