try to avoid memory leak
Build and Push Docker Images / build-and-push (push) Successful in 1m47s

This commit is contained in:
2025-06-06 14:11:47 +02:00
parent e2cfcfad34
commit ac0b6c061e
+145 -47
View File
@@ -35,8 +35,10 @@ CUSTOM_USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.3
# Browser pool configuration # Browser pool configuration
MAX_BROWSERS = int(os.getenv('MAX_BROWSERS', '5')) # Maximum number of browser instances MAX_BROWSERS = int(os.getenv('MAX_BROWSERS', '5')) # Maximum number of browser instances
BROWSER_TTL = int(os.getenv('BROWSER_TTL', '3600')) # Time to live for browser instances in seconds
browser_pool = Queue() browser_pool = Queue()
browser_lock = Lock() browser_lock = Lock()
browser_creation_times = {}
# Rate limiting configuration # Rate limiting configuration
RATE_LIMIT_MINUTE = int(os.getenv('RATE_LIMIT_MINUTE', '60')) # requests per minute RATE_LIMIT_MINUTE = int(os.getenv('RATE_LIMIT_MINUTE', '60')) # requests per minute
@@ -86,48 +88,80 @@ app.add_middleware(RateLimitMiddleware)
async def create_browser(): async def create_browser():
"""Create a new browser instance""" """Create a new browser instance"""
return await launch( browser = await launch(
headless=True, headless=True,
executablePath='/usr/bin/google-chrome', executablePath='/usr/bin/google-chrome',
args=['--no-sandbox', '--disable-setuid-sandbox'], args=[
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--disable-gpu',
'--disable-extensions',
'--disable-sync',
'--disable-background-networking',
'--disable-default-apps',
'--disable-translate',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-client-side-phishing-detection',
'--disable-features=site-per-process',
'--disable-hang-monitor',
'--disable-ipc-flooding-protection',
'--disable-popup-blocking',
'--disable-prompt-on-repost',
'--disable-renderer-backgrounding',
'--memory-pressure-off',
'--no-first-run',
'--safebrowsing-disable-auto-update',
],
handleSIGINT=False, handleSIGINT=False,
handleSIGTERM=False, handleSIGTERM=False,
handleSIGHUP=False handleSIGHUP=False,
ignoreHTTPSErrors=True
) )
browser_creation_times[browser] = time.time()
return browser
@asynccontextmanager async def check_browser_health():
async def get_browser(): """Check browser health and recycle if needed"""
"""Get a browser from the pool or create a new one if needed""" while True:
browser = None
try:
# Try to get a browser from the pool
try: try:
browser = await browser_pool.get_nowait() # Sleep for 5 minutes between checks
except asyncio.QueueEmpty: await asyncio.sleep(300)
# 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 async with browser_lock:
finally: # Get all browsers from the pool
# Return browser to pool if it's still viable browsers = []
if browser: while not browser_pool.empty():
try: browsers.append(await browser_pool.get())
# Quick check if browser is still usable
await browser.pages() # Check each browser
await browser_pool.put(browser) for browser in browsers:
except Exception: try:
# If browser is not usable, close it and create a new one # Check if browser is too old
try: if time.time() - browser_creation_times.get(browser, 0) > BROWSER_TTL:
await browser.close() await browser.close()
except: del browser_creation_times[browser]
pass browser = await create_browser()
browser = await create_browser() else:
await browser_pool.put(browser) # Quick health check
await browser.pages()
# Put back in pool if healthy
await browser_pool.put(browser)
except Exception:
# If unhealthy, close and create new
try:
await browser.close()
except:
pass
if browser in browser_creation_times:
del browser_creation_times[browser]
new_browser = await create_browser()
await browser_pool.put(new_browser)
except Exception as e:
print(f"Error in browser health check: {str(e)}")
# Initialize browser pool # Initialize browser pool
@app.on_event("startup") @app.on_event("startup")
@@ -138,6 +172,9 @@ async def init_browser_pool():
await browser_pool.put(browser) await browser_pool.put(browser)
print(f"Browser pool initialized with {browser_pool.qsize()} browsers") print(f"Browser pool initialized with {browser_pool.qsize()} browsers")
# Start browser health check task
asyncio.create_task(check_browser_health())
@app.on_event("shutdown") @app.on_event("shutdown")
async def cleanup_browser_pool(): async def cleanup_browser_pool():
"""Clean up all browsers in the pool""" """Clean up all browsers in the pool"""
@@ -145,6 +182,8 @@ async def cleanup_browser_pool():
try: try:
browser = await browser_pool.get_nowait() browser = await browser_pool.get_nowait()
await browser.close() await browser.close()
if browser in browser_creation_times:
del browser_creation_times[browser]
except: except:
pass pass
@@ -332,30 +371,47 @@ async def health_check():
return {"status": "ok"} return {"status": "ok"}
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 a browser operation with proper cleanup"""
async with get_browser() as browser: async with get_browser() as browser:
try: try:
# Create new page with timeout # Create a new page
page = await browser.newPage() page = await browser.newPage()
page.setDefaultNavigationTimeout(30000)
# Set custom user agent # Set reasonable viewport
await page.setViewport({'width': 1280, 'height': 800})
# Set user agent
await page.setUserAgent(CUSTOM_USER_AGENT) await page.setUserAgent(CUSTOM_USER_AGENT)
# Call the operation function that uses the page # Set reasonable timeout
result = await operation_func(page) page.setDefaultNavigationTimeout(30000)
# Explicitly close the page # Enable request interception to block unnecessary resources
await page.close() await page.setRequestInterception(True)
async def intercept(request):
# Block unnecessary resource types
if request.resourceType in ['image', 'media', 'font', 'stylesheet']:
await request.abort()
else:
await request.continue_()
page.on('request', lambda req: asyncio.ensure_future(intercept(req)))
# Perform the operation
result = await operation_func(page)
return result return result
except Exception as e: except Exception as e:
# If there's an error, close the page and re-raise print(f"Error during browser operation: {str(e)}")
raise
finally:
try: try:
await page.close() # Ensure page is properly closed
except: if 'page' in locals():
pass await page.close()
raise e except Exception as e:
print(f"Error closing page: {str(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)):
@@ -1071,6 +1127,48 @@ async def detect_pagination(url: str, x_api_key: Optional[str] = Header(None)):
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
@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()
# Check if browser is too old
if time.time() - browser_creation_times.get(browser, 0) > BROWSER_TTL:
await browser.close()
if browser in browser_creation_times:
del browser_creation_times[browser]
browser = await create_browser()
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
if browser in browser_creation_times:
del browser_creation_times[browser]
browser = await create_browser()
await browser_pool.put(browser)
if __name__ == "__main__": if __name__ == "__main__":
import uvicorn import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000) uvicorn.run(app, host="0.0.0.0", port=8000)