Skip to content

Commit fd9e89f

Browse files
kennethreitzclaude
andcommitted
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>
1 parent 5514218 commit fd9e89f

File tree

3 files changed

+60
-31
lines changed

3 files changed

+60
-31
lines changed

kjvstudy_org/routes/about.py

Lines changed: 44 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""About routes - stats, cross-references index, and about page."""
2+
import asyncio
23
import json
34
import re
45
from collections import defaultdict
@@ -22,12 +23,11 @@ def init_templates(t: Jinja2Templates):
2223

2324

2425
# =============================================================================
25-
# Routes
26+
# Helper Functions (run in thread pool)
2627
# =============================================================================
2728

28-
@router.get("/about/stats", response_class=HTMLResponse)
29-
async def stats(request: Request):
30-
"""Hidden statistics page - comprehensive site metrics"""
29+
def _compute_stats() -> dict:
30+
"""Compute all statistics - runs in thread pool to avoid blocking."""
3131
data_dir = Path(__file__).parent.parent / "data"
3232

3333
# Bible statistics
@@ -148,7 +148,7 @@ async def stats(request: Request):
148148
total_hebrew_entries = 0
149149
total_greek_entries = 0
150150

151-
stats_data = {
151+
return {
152152
'bible': {
153153
'total_verses': total_verses,
154154
'total_books': total_books,
@@ -207,27 +207,9 @@ async def stats(request: Request):
207207
}
208208
}
209209

210-
books = bible.get_books()
211-
breadcrumbs = [
212-
{"text": "Home", "url": "/"},
213-
{"text": "About", "url": "/about"},
214-
{"text": "Statistics", "url": None}
215-
]
216-
217-
return templates.TemplateResponse(
218-
"stats.html",
219-
{
220-
"request": request,
221-
"books": books,
222-
"stats": stats_data,
223-
"breadcrumbs": breadcrumbs,
224-
}
225-
)
226-
227210

228-
@router.get("/about/cross-references", response_class=HTMLResponse)
229-
async def cross_references_index(request: Request):
230-
"""Cross-references index - list all verses with cross-references"""
211+
def _compute_crossref_index() -> tuple:
212+
"""Compute cross-reference index - runs in thread pool."""
231213
data_dir = Path(__file__).parent.parent / "data" / "cross_references"
232214

233215
# Build index of all verses with cross-references, grouped by book
@@ -273,6 +255,43 @@ async def cross_references_index(request: Request):
273255
for verses in chapters.values()
274256
)
275257

258+
return crossref_index, total_books, total_verses, total_refs
259+
260+
261+
# =============================================================================
262+
# Routes
263+
# =============================================================================
264+
265+
@router.get("/about/stats", response_class=HTMLResponse)
266+
async def stats(request: Request):
267+
"""Hidden statistics page - comprehensive site metrics"""
268+
# Run heavy computation in thread pool
269+
stats_data = await asyncio.to_thread(_compute_stats)
270+
271+
books = bible.get_books()
272+
breadcrumbs = [
273+
{"text": "Home", "url": "/"},
274+
{"text": "About", "url": "/about"},
275+
{"text": "Statistics", "url": None}
276+
]
277+
278+
return templates.TemplateResponse(
279+
"stats.html",
280+
{
281+
"request": request,
282+
"books": books,
283+
"stats": stats_data,
284+
"breadcrumbs": breadcrumbs,
285+
}
286+
)
287+
288+
289+
@router.get("/about/cross-references", response_class=HTMLResponse)
290+
async def cross_references_index(request: Request):
291+
"""Cross-references index - list all verses with cross-references"""
292+
# Run heavy I/O in thread pool
293+
crossref_index, total_books, total_verses, total_refs = await asyncio.to_thread(_compute_crossref_index)
294+
276295
books = bible.get_books()
277296
breadcrumbs = [
278297
{"text": "Home", "url": "/"},

kjvstudy_org/routes/commentary.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
- Helper functions for generating theological commentary
66
- Book summaries, chapter overviews, and verse analysis
77
"""
8+
import asyncio
89
import json
910
import random
11+
from collections import defaultdict
1012
from functools import lru_cache
1113
from pathlib import Path
1214
from fastapi import APIRouter, Request, HTTPException
@@ -20,10 +22,8 @@
2022
templates = None
2123

2224

23-
@router.get("/about/commentary", response_class=HTMLResponse)
24-
async def commentary_index(request: Request):
25-
"""Commentary index - list all verses with commentary"""
26-
from collections import defaultdict
25+
def _compute_commentary_index() -> tuple:
26+
"""Compute commentary index - runs in thread pool."""
2727
from ..utils.books import OT_BOOKS, NT_BOOKS
2828

2929
data_dir = Path(__file__).parent.parent / "data" / "verse_commentary"
@@ -58,6 +58,15 @@ async def commentary_index(request: Request):
5858
for verses in chapters.values()
5959
)
6060

61+
return commentary_index, total_books, total_verses
62+
63+
64+
@router.get("/about/commentary", response_class=HTMLResponse)
65+
async def commentary_index(request: Request):
66+
"""Commentary index - list all verses with commentary"""
67+
# Run heavy I/O in thread pool
68+
commentary_idx, total_books, total_verses = await asyncio.to_thread(_compute_commentary_index)
69+
6170
breadcrumbs = [
6271
{"text": "Home", "url": "/"},
6372
{"text": "About", "url": "/about"},
@@ -73,7 +82,7 @@ async def commentary_index(request: Request):
7382
{
7483
"request": request,
7584
"books": books,
76-
"commentary_index": commentary_index,
85+
"commentary_index": commentary_idx,
7786
"total_books": total_books,
7887
"total_verses": total_verses,
7988
"breadcrumbs": breadcrumbs,

kjvstudy_org/routes/misc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -413,7 +413,8 @@ async def og_image_verse(
413413
# Return default image if verse not found
414414
from pathlib import Path as PathLib
415415
default_path = PathLib(__file__).parent.parent / "static" / "og-image.png"
416-
return Response(content=default_path.read_bytes(), media_type="image/png")
416+
content = await asyncio.to_thread(default_path.read_bytes)
417+
return Response(content=content, media_type="image/png")
417418

418419
title = f"{book} {chapter}:{verse}"
419420
cache_key = f"verse:{book}:{chapter}:{verse}"

0 commit comments

Comments
 (0)