adjust puppeteer
Build and Push Docker Images / build-and-push (push) Successful in 4m12s

This commit is contained in:
2025-05-30 13:21:00 +02:00
parent 2d9f236798
commit 5d0ab2ebd0
2 changed files with 144 additions and 30 deletions
+5
View File
@@ -44,6 +44,11 @@ RUN useradd -m puppeteer && chown -R puppeteer:puppeteer /app
# Create the database directory and set permissions # Create the database directory and set permissions
RUN mkdir -p /db && chmod 777 /db && chown -R puppeteer:puppeteer /app RUN mkdir -p /db && chmod 777 /db && chown -R puppeteer:puppeteer /app
# Add system limits configuration
RUN echo "* soft nofile 65535" >> /etc/security/limits.conf && \
echo "* hard nofile 65535" >> /etc/security/limits.conf && \
echo "session required pam_limits.so" >> /etc/pam.d/common-session
USER puppeteer USER puppeteer
# Expose port # Expose port
+139 -30
View File
@@ -1,4 +1,4 @@
from fastapi import FastAPI, HTTPException, Header from fastapi import FastAPI, HTTPException, Header, Request
from pyppeteer import launch from pyppeteer import launch
import os import os
import asyncio import asyncio
@@ -11,6 +11,11 @@ from typing import Optional, Dict, List, Any
from urllib.parse import unquote from urllib.parse import unquote
from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.cron import CronTrigger from apscheduler.triggers.cron import CronTrigger
from fastapi.middleware.base import BaseHTTPMiddleware
# Add imports for browser pool
from asyncio import Queue, Lock
from contextlib import asynccontextmanager
app = FastAPI() app = FastAPI()
@@ -28,6 +33,121 @@ CLEANUP_CRON = os.getenv('CLEANUP_CRON', '0 3 * * *')
# Define custom user agent # Define custom user agent
CUSTOM_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36' CUSTOM_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Safari/537.36'
# Browser pool configuration
MAX_BROWSERS = int(os.getenv('MAX_BROWSERS', '5')) # Maximum number of browser instances
browser_pool = Queue()
browser_lock = Lock()
# Rate limiting configuration
RATE_LIMIT_MINUTE = int(os.getenv('RATE_LIMIT_MINUTE', '60')) # requests per minute
RATE_LIMIT_WINDOW = 60 # window size in seconds
class RateLimitMiddleware(BaseHTTPMiddleware):
def __init__(self, app):
super().__init__(app)
self.requests = {}
self.lock = asyncio.Lock()
async def dispatch(self, request: Request, call_next):
# Skip rate limiting for health check
if request.url.path == "/" and request.method == "HEAD":
return await call_next(request)
api_key = request.headers.get("x-api-key")
if not api_key:
raise HTTPException(status_code=401, detail="API key required")
async with self.lock:
now = time.time()
# Clean old requests
self.requests = {k: v for k, v in self.requests.items()
if now - v[-1] < RATE_LIMIT_WINDOW}
# Get request times for this API key
requests = self.requests.get(api_key, [])
# Remove old requests outside the window
requests = [t for t in requests if now - t < RATE_LIMIT_WINDOW]
if len(requests) >= RATE_LIMIT_MINUTE:
oldest = requests[0]
wait_time = RATE_LIMIT_WINDOW - (now - oldest)
raise HTTPException(
status_code=429,
detail=f"Rate limit exceeded. Try again in {int(wait_time)} seconds"
)
requests.append(now)
self.requests[api_key] = requests
return await call_next(request)
# Add rate limiting middleware
app.add_middleware(RateLimitMiddleware)
async def create_browser():
"""Create a new browser instance"""
return await launch(
headless=True,
executablePath='/usr/bin/google-chrome',
args=['--no-sandbox', '--disable-setuid-sandbox'],
handleSIGINT=False,
handleSIGTERM=False,
handleSIGHUP=False
)
@asynccontextmanager
async def get_browser():
"""Get a browser from the pool or create a new one if needed"""
browser = None
try:
# Try to get a browser from the pool
try:
browser = await browser_pool.get_nowait()
except asyncio.QueueEmpty:
# If pool is empty, create a new browser if under the limit
async with browser_lock:
if browser_pool.qsize() + 1 <= MAX_BROWSERS:
browser = await create_browser()
else:
# If at limit, wait for a browser to become available
browser = await browser_pool.get()
yield browser
finally:
# Return browser to pool if it's still viable
if browser:
try:
# Quick check if browser is still usable
await browser.pages()
await browser_pool.put(browser)
except Exception:
# If browser is not usable, close it and create a new one
try:
await browser.close()
except:
pass
browser = await create_browser()
await browser_pool.put(browser)
# Initialize browser pool
@app.on_event("startup")
async def init_browser_pool():
"""Initialize the browser pool with some browsers"""
for _ in range(min(3, MAX_BROWSERS)): # Start with 3 browsers or MAX_BROWSERS, whichever is smaller
browser = await create_browser()
await browser_pool.put(browser)
print(f"Browser pool initialized with {browser_pool.qsize()} browsers")
@app.on_event("shutdown")
async def cleanup_browser_pool():
"""Clean up all browsers in the pool"""
while not browser_pool.empty():
try:
browser = await browser_pool.get_nowait()
await browser.close()
except:
pass
# Initialize SQLite database # Initialize SQLite database
def init_db(): def init_db():
global DB_PATH global DB_PATH
@@ -213,40 +333,29 @@ async def health_check():
async def safe_browser_operation(url, operation_func): async def safe_browser_operation(url, operation_func):
"""Safely perform browser operations with proper cleanup""" """Safely perform browser operations with proper cleanup"""
browser = None async with get_browser() as browser:
try: try:
browser = await launch( # Create new page with timeout
headless=True, page = await browser.newPage()
executablePath='/usr/bin/google-chrome', page.setDefaultNavigationTimeout(30000)
args=['--no-sandbox', '--disable-setuid-sandbox'],
handleSIGINT=False,
handleSIGTERM=False,
handleSIGHUP=False
)
# Create new page with timeout # Set custom user agent
page = await browser.newPage() await page.setUserAgent(CUSTOM_USER_AGENT)
page.setDefaultNavigationTimeout(30000)
# Set custom user agent # Call the operation function that uses the page
await page.setUserAgent(CUSTOM_USER_AGENT) result = await operation_func(page)
# Call the operation function that uses the page # Explicitly close the page
result = await operation_func(page) await page.close()
# Explicitly close the page return result
await page.close() except Exception as e:
# If there's an error, close the page and re-raise
return result
finally:
# Ensure browser is closed properly
if browser:
try: try:
await browser.close() await page.close()
except Exception as e: except:
print(f"Error closing browser: {e}") pass
# We don't re-raise here to avoid masking the original error raise e
@app.get("/") @app.get("/")
async def visit_url(url: str, x_api_key: Optional[str] = Header(None)): async def visit_url(url: str, x_api_key: Optional[str] = Header(None)):