fix: add asyncio.Lock to get_browser() and deduplicate font-face blocks
All checks were successful
Build Docker Image / build (pull_request) Successful in 3m32s

Address two review findings from PR #94:

1. Race condition: concurrent requests could both launch Chromium when
   _browser is None. Wrap the init check in asyncio.Lock so only one
   coroutine creates the browser process.

2. Font duplication: the WOFF2 files are variable fonts covering all
   needed weights. Consolidate 5 @font-face blocks (3x Open Sans,
   2x Source Sans 3) into 2 using CSS font-weight range syntax,
   saving ~163KB of redundant base64 per render.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Cal Corum 2026-03-13 08:28:58 -05:00
parent c262bb431e
commit 6ab50ba5f2
2 changed files with 19 additions and 33 deletions

View File

@ -9,6 +9,8 @@ from typing import Optional, List, Literal
import logging import logging
import pydantic import pydantic
from pandas import DataFrame from pandas import DataFrame
import asyncio as _asyncio
from playwright.async_api import async_playwright, Browser, Playwright from playwright.async_api import async_playwright, Browser, Playwright
from ..card_creation import get_batter_card_data, get_pitcher_card_data from ..card_creation import get_batter_card_data, get_pitcher_card_data
@ -37,6 +39,7 @@ from ..dependencies import oauth2_scheme, valid_token
_browser: Browser | None = None _browser: Browser | None = None
_playwright: Playwright | None = None _playwright: Playwright | None = None
_browser_lock = _asyncio.Lock()
async def get_browser() -> Browser: async def get_browser() -> Browser:
@ -45,18 +48,22 @@ async def get_browser() -> Browser:
Reuses a single browser across all card renders, eliminating the ~1-1.5s Reuses a single browser across all card renders, eliminating the ~1-1.5s
per-request launch/teardown overhead. Automatically reconnects if the per-request launch/teardown overhead. Automatically reconnects if the
browser process has died. browser process has died.
Uses an asyncio.Lock to prevent concurrent requests from racing to
launch multiple Chromium processes.
""" """
global _browser, _playwright global _browser, _playwright
if _browser is None or not _browser.is_connected(): async with _browser_lock:
if _playwright is not None: if _browser is None or not _browser.is_connected():
try: if _playwright is not None:
await _playwright.stop() try:
except Exception: await _playwright.stop()
pass except Exception:
_playwright = await async_playwright().start() pass
_browser = await _playwright.chromium.launch( _playwright = await async_playwright().start()
args=["--no-sandbox", "--disable-dev-shm-usage"] _browser = await _playwright.chromium.launch(
) args=["--no-sandbox", "--disable-dev-shm-usage"]
)
return _browser return _browser

File diff suppressed because one or more lines are too long