potential performance upgrade
Build and Push Docker Images / build-and-push (push) Successful in 21s

This commit is contained in:
2025-07-18 16:19:26 +02:00
parent 11259502f1
commit e51894361f
3 changed files with 433 additions and 196 deletions
@@ -12,42 +12,49 @@ async def wait_for_network_idle(page):
await page.wait_for_load_state('networkidle') await page.wait_for_load_state('networkidle')
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 and timeouts"""
browser = None browser = None
context = None context = None
page = None page = None
playwright = None playwright = None
try: try:
# Get or create playwright instance # Get or create playwright instance with timeout
playwright = await async_playwright().start() playwright = await asyncio.wait_for(async_playwright().start(), timeout=30.0)
browser = await playwright.chromium.launch( browser = await asyncio.wait_for(playwright.chromium.launch(
headless=True, headless=True,
args=['--no-sandbox', '--disable-setuid-sandbox'], args=['--no-sandbox', '--disable-setuid-sandbox', '--max_old_space_size=256'],
) ), timeout=30.0)
# Create context and page # Create context and page with timeout
context = await browser.new_context( context = await asyncio.wait_for(browser.new_context(
user_agent=CUSTOM_USER_AGENT, user_agent=CUSTOM_USER_AGENT,
viewport={'width': 1920, 'height': 1080}, viewport={'width': 1920, 'height': 1080},
ignore_https_errors=True, ignore_https_errors=True,
) ), timeout=10.0)
page = await context.new_page() page = await asyncio.wait_for(context.new_page(), timeout=10.0)
page.set_default_timeout(30000) page.set_default_timeout(30000) # 30 second timeout
# Track page creation time for force cleanup # Track page creation time for force cleanup
page_creation_times[page] = time.time() page_creation_times[page] = time.time()
active_pages.add(page) active_pages.add(page)
# Call the operation function that uses the page # Call the operation function that uses the page with timeout
result = await operation_func(page) result = await asyncio.wait_for(operation_func(page), timeout=60.0)
return result return result
except asyncio.TimeoutError as e:
print(f"Browser operation timeout for {url}: {e}")
return {
"status": "error",
"error": f"Operation timeout: {str(e)}",
"url": url
}
except Exception as e: except Exception as e:
print(f"Error during browser operation: {e}") print(f"Error during browser operation for {url}: {e}")
# Return error result instead of re-raising to allow graceful handling # Return error result instead of re-raising to allow graceful handling
return { return {
"status": "error", "status": "error",
@@ -55,7 +62,7 @@ async def safe_browser_operation(url, operation_func):
"url": url "url": url
} }
finally: finally:
# Cleanup page tracking # Cleanup page tracking with timeout
if page: if page:
try: try:
# Remove from tracking # Remove from tracking
@@ -63,29 +70,38 @@ async def safe_browser_operation(url, operation_func):
active_pages.remove(page) active_pages.remove(page)
if page in page_creation_times: if page in page_creation_times:
del page_creation_times[page] del page_creation_times[page]
await page.close() await asyncio.wait_for(page.close(), timeout=5.0)
except Exception as e: except (asyncio.TimeoutError, Exception) as e:
print(f"Error closing page: {e}") print(f"Error closing page: {e}")
try:
await page.close(force=True)
except:
pass
# Ensure context is closed properly # Ensure context is closed properly with timeout
if context: if context:
try: try:
await context.close() await asyncio.wait_for(context.close(), timeout=5.0)
except Exception as e: except (asyncio.TimeoutError, Exception) as e:
print(f"Error closing context: {e}") print(f"Error closing context: {e}")
# Ensure browser is closed properly # Ensure browser is closed properly with timeout
if browser: if browser:
try: try:
await browser.close() await asyncio.wait_for(browser.close(), timeout=10.0)
except asyncio.TimeoutError:
try:
await browser.close(force=True)
except:
pass
except Exception as e: except Exception as e:
print(f"Error closing browser: {e}") print(f"Error closing browser: {e}")
# Ensure playwright is closed properly - THIS IS THE KEY FIX # Ensure playwright is closed properly with timeout - THIS IS THE KEY FIX
if playwright: if playwright:
try: try:
await playwright.stop() await asyncio.wait_for(playwright.stop(), timeout=10.0)
except Exception as e: except (asyncio.TimeoutError, Exception) as e:
print(f"Error closing playwright: {e}") print(f"Error closing playwright: {e}")
async def force_cleanup_old_pages(): async def force_cleanup_old_pages():
@@ -273,73 +273,101 @@ class CloudflareBypass:
""") """)
@staticmethod @staticmethod
async def handle_cloudflare_challenge(page: Page, max_retries: int = 3) -> bool: async def handle_cloudflare_challenge(page: Page, max_retries: int = 2) -> bool:
"""Handle Cloudflare challenges and wait for them to complete""" """Handle Cloudflare challenge with improved efficiency"""
for attempt in range(max_retries): for attempt in range(max_retries):
try: try:
# Check for Cloudflare challenge print(f"Handling Cloudflare challenge (attempt {attempt + 1}/{max_retries})")
cloudflare_selectors = [
'#challenge-form',
'#cf-please-wait',
'.cf-browser-verification',
'#cf-wrapper',
'iframe[src*="cloudflare"]'
]
for selector in cloudflare_selectors: # Wait for challenge form with shorter timeout
try: try:
element = await page.wait_for_selector(selector, timeout=5000) await page.wait_for_selector('#challenge-form', timeout=10000) # 10 seconds
if element:
print(f"Cloudflare challenge detected on attempt {attempt + 1}")
# Wait for challenge to complete
await asyncio.sleep(5 + random.uniform(2, 8))
break
except: except:
continue # If no challenge form, might already be bypassed
# Wait for page to load completely
await page.wait_for_load_state('networkidle', timeout=30000)
# Check if we're past the challenge
title = await page.title()
if 'Cloudflare' not in title and 'challenge' not in title.lower():
print("Successfully bypassed Cloudflare challenge")
return True return True
# Add some random delay # Wait for the challenge to complete with shorter timeout
await asyncio.sleep(random.uniform(3, 8)) try:
await page.wait_for_function('''
() => {
return !document.querySelector('#challenge-form') &&
!document.querySelector('#cf-wrapper') &&
!document.querySelector('#cf-please-wait') &&
!document.querySelector('.cf-browser-verification');
}
''', timeout=15000) # 15 seconds
print("Cloudflare challenge completed")
return True
except Exception as e:
print(f"Challenge completion timeout (attempt {attempt + 1}): {e}")
# Try to click any "Verify" buttons
try:
verify_buttons = await page.query_selector_all('button[type="submit"], input[type="submit"]')
for button in verify_buttons:
try:
await button.click(timeout=5000)
await asyncio.sleep(2)
except:
continue
except:
pass
# Short delay before retry
if attempt < max_retries - 1:
await asyncio.sleep(2)
except Exception as e: except Exception as e:
print(f"Error handling Cloudflare challenge (attempt {attempt + 1}): {e}") print(f"Error handling Cloudflare challenge (attempt {attempt + 1}): {e}")
if attempt < max_retries - 1: if attempt < max_retries - 1:
await asyncio.sleep(random.uniform(5, 15)) await asyncio.sleep(2)
print("Failed to bypass Cloudflare challenge after all attempts")
return False return False
@staticmethod @staticmethod
async def navigate_with_stealth(page: Page, url: str) -> bool: async def navigate_with_stealth(page: Page, url: str) -> bool:
"""Navigate to URL with stealth measures and handle Cloudflare""" """Navigate to URL with stealth measures and improved performance"""
try: try:
# Add random delay before navigation # Set shorter timeouts for better performance
await asyncio.sleep(random.uniform(1, 3)) page.set_default_timeout(30000) # 30 seconds
page.set_default_navigation_timeout(30000) # 30 seconds
# Navigate to the URL # Navigate with stealth measures
await page.goto(url, wait_until='domcontentloaded') response = await page.goto(url, wait_until='domcontentloaded') # Changed from 'networkidle' to 'domcontentloaded' for speed
# Handle Cloudflare challenge if not response:
success = await CloudflareBypass.handle_cloudflare_challenge(page)
if not success:
print("Failed to bypass Cloudflare challenge")
return False return False
# Add human-like scrolling # Quick check for Cloudflare challenge
await CloudflareBypass._simulate_human_scrolling(page) if response.status == 403 or response.status == 503:
# Handle Cloudflare challenge
return await CloudflareBypass.handle_cloudflare_challenge(page, max_retries=2) # Reduced retries
# Check for Cloudflare indicators in the page
cloudflare_detected = await page.evaluate('''() => {
return document.title.toLowerCase().includes('cloudflare') ||
!!document.querySelector('#challenge-form') ||
!!document.querySelector('#cf-wrapper') ||
!!document.querySelector('#cf-please-wait') ||
!!document.querySelector('.cf-browser-verification');
}''')
if cloudflare_detected:
return await CloudflareBypass.handle_cloudflare_challenge(page, max_retries=2) # Reduced retries
# Wait for page to be ready (shorter wait)
try:
await page.wait_for_load_state('domcontentloaded', timeout=10000) # 10 second timeout
except:
pass # Continue even if load state timeout
return True return True
except Exception as e: except Exception as e:
print(f"Error during stealth navigation: {e}") print(f"Error during stealth navigation to {url}: {e}")
return False return False
@staticmethod @staticmethod
+263 -70
View File
@@ -22,6 +22,9 @@ from contextlib import asynccontextmanager
from app.utils.browser_utils import force_cleanup_old_pages from app.utils.browser_utils import force_cleanup_old_pages
from app.utils.cloudflare_bypass import CloudflareBypass from app.utils.cloudflare_bypass import CloudflareBypass
# Add memory monitoring
import gc
app = FastAPI() app = FastAPI()
# Get API key from environment variable # Get API key from environment variable
@@ -39,9 +42,11 @@ CLEANUP_CRON = os.getenv('CLEANUP_CRON', '0 3 * * *')
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 - Reduced for better resource management # Browser pool configuration - Reduced for better resource management
MAX_BROWSERS = int(os.getenv('MAX_BROWSERS', '3')) # Reduced from 5 to 3 MAX_BROWSERS = int(os.getenv('MAX_BROWSERS', '2')) # Reduced from 3 to 2 for better resource management
BROWSER_TTL = int(os.getenv('BROWSER_TTL', '1800')) # Reduced from 3600 to 1800 seconds (30 minutes) BROWSER_TTL = int(os.getenv('BROWSER_TTL', '900')) # Reduced from 1800 to 900 seconds (15 minutes)
MAX_CONCURRENT_OPERATIONS = int(os.getenv('MAX_CONCURRENT_OPERATIONS', '5')) # New: limit concurrent operations MAX_CONCURRENT_OPERATIONS = int(os.getenv('MAX_CONCURRENT_OPERATIONS', '3')) # Reduced from 5 to 3
BROWSER_OPERATION_TIMEOUT = int(os.getenv('BROWSER_OPERATION_TIMEOUT', '60')) # New: timeout for browser operations
PAGE_TIMEOUT = int(os.getenv('PAGE_TIMEOUT', '30')) # New: timeout for page operations
# Browser pool management # Browser pool management
browser_pool = Queue(maxsize=MAX_BROWSERS) # Add maxsize to prevent unbounded growth browser_pool = Queue(maxsize=MAX_BROWSERS) # Add maxsize to prevent unbounded growth
@@ -52,6 +57,7 @@ active_browsers = set() # Track active browsers
active_pages = set() # Track active pages for force cleanup active_pages = set() # Track active pages for force cleanup
operation_semaphore = Semaphore(MAX_CONCURRENT_OPERATIONS) # Limit concurrent operations operation_semaphore = Semaphore(MAX_CONCURRENT_OPERATIONS) # Limit concurrent operations
playwright_instance = None # Global playwright instance playwright_instance = None # Global playwright instance
browser_health_check_task = None # Track health check task
# 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
@@ -111,9 +117,48 @@ async def create_browser():
proxy_url = os.getenv('PROXY_URL') proxy_url = os.getenv('PROXY_URL')
proxy_config = CloudflareBypass.get_proxy_config(proxy_url) proxy_config = CloudflareBypass.get_proxy_config(proxy_url)
# Add memory and performance optimizations
browser_args = CloudflareBypass.get_stealth_args() + [
'--memory-pressure-off',
'--max_old_space_size=256', # Reduced memory usage
'--disable-dev-shm-usage',
'--disable-gpu-sandbox',
'--disable-software-rasterizer',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding',
'--disable-features=TranslateUI',
'--disable-ipc-flooding-protection',
'--disable-hang-monitor',
'--disable-prompt-on-repost',
'--disable-sync',
'--no-first-run',
'--no-default-browser-check',
'--no-pings',
'--no-zygote',
'--single-process',
'--disable-background-networking',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-breakpad',
'--disable-component-extensions-with-background-pages',
'--disable-dev-shm-usage',
'--disable-features=TranslateUI',
'--disable-ipc-flooding-protection',
'--disable-renderer-backgrounding',
'--disable-sync',
'--force-color-profile=srgb',
'--metrics-recording-only',
'--no-first-run',
'--safebrowsing-disable-auto-update',
'--enable-automation',
'--password-store=basic',
'--use-mock-keychain',
]
browser = await playwright_instance.chromium.launch( browser = await playwright_instance.chromium.launch(
headless=True, headless=True,
args=CloudflareBypass.get_stealth_args(), args=browser_args,
ignore_default_args=['--enable-automation'], ignore_default_args=['--enable-automation'],
**proxy_config **proxy_config
) )
@@ -124,10 +169,17 @@ async def create_browser():
return browser return browser
except Exception as e: except Exception as e:
print(f"Error creating browser: {e}") print(f"Error creating browser: {e}")
# Clean up playwright instance if browser creation fails
if playwright_instance:
try:
await playwright_instance.stop()
playwright_instance = None
except:
pass
raise raise
async def cleanup_browser(browser): async def cleanup_browser(browser):
"""Safely cleanup a browser instance""" """Safely cleanup a browser instance with improved error handling"""
try: try:
if browser in active_browsers: if browser in active_browsers:
active_browsers.remove(browser) active_browsers.remove(browser)
@@ -136,33 +188,66 @@ async def cleanup_browser(browser):
del browser_creation_times[browser] del browser_creation_times[browser]
# Close all pages first and clean up page tracking # Close all pages first and clean up page tracking
pages = browser.contexts[0].pages if browser.contexts else [] try:
contexts = browser.contexts
for context in contexts:
pages = context.pages
for page in pages: for page in pages:
try: try:
if page in active_pages: if page in active_pages:
active_pages.remove(page) active_pages.remove(page)
if page in page_creation_times: if page in page_creation_times:
del page_creation_times[page] del page_creation_times[page]
await page.close() await asyncio.wait_for(page.close(), timeout=5.0)
except Exception as e: except (asyncio.TimeoutError, Exception) as e:
print(f"Error closing page: {e}") print(f"Error closing page: {e}")
# Force close if normal close fails
try:
await page.close(force=True)
except:
pass
# Close context
try:
await asyncio.wait_for(context.close(), timeout=5.0)
except (asyncio.TimeoutError, Exception) as e:
print(f"Error closing context: {e}")
except Exception as e:
print(f"Error during context/page cleanup: {e}")
# Close browser with timeout
try:
await asyncio.wait_for(browser.close(), timeout=10.0)
except asyncio.TimeoutError:
print("Browser close timeout, forcing close")
try:
await browser.close(force=True)
except:
pass
except Exception as e:
print(f"Error during browser cleanup: {e}")
# Close browser
await browser.close()
print(f"Browser cleaned up. Total active browsers: {len(active_browsers)}") print(f"Browser cleaned up. Total active browsers: {len(active_browsers)}")
except Exception as e: except Exception as e:
print(f"Error during browser cleanup: {e}") print(f"Error during browser cleanup: {e}")
async def check_browser_health(): async def check_browser_health():
"""Check browser health and recycle if needed""" """Check browser health and recycle if needed with improved efficiency"""
global browser_health_check_task
while True: while True:
try: try:
# Sleep for 2 minutes between checks (reduced from 5 minutes) # Sleep for 1 minute between checks (reduced from 2 minutes)
await asyncio.sleep(120) await asyncio.sleep(60)
# Monitor memory usage
await monitor_memory_usage()
# First, force cleanup old instances # First, force cleanup old instances
await force_cleanup_old_instances() await force_cleanup_old_instances()
# Use a timeout for the entire health check operation
async with asyncio.timeout(30): # 30 second timeout for health check
async with browser_lock: async with browser_lock:
# Get all browsers from the pool # Get all browsers from the pool
browsers = [] browsers = []
@@ -181,10 +266,18 @@ async def check_browser_health():
await cleanup_browser(browser) await cleanup_browser(browser)
browser = await create_browser() browser = await create_browser()
else: else:
# Quick health check # Quick health check - just verify browser is still responsive
try:
contexts = browser.contexts contexts = browser.contexts
if contexts: if not contexts:
pages = contexts[0].pages # Browser has no contexts, might be dead
print("Browser has no contexts, recycling")
await cleanup_browser(browser)
browser = await create_browser()
except Exception as e:
print(f"Browser health check failed: {e}")
await cleanup_browser(browser)
browser = await create_browser()
# Put back in pool if healthy # Put back in pool if healthy
if not browser_pool.full(): if not browser_pool.full():
@@ -200,18 +293,12 @@ async def check_browser_health():
except: except:
pass pass
# Create new browsers if pool is empty except asyncio.TimeoutError:
while browser_pool.qsize() < MAX_BROWSERS: print("Browser health check timed out, continuing...")
try:
browser = await create_browser()
await browser_pool.put(browser)
except Exception as e:
print(f"Error creating browser for pool: {e}")
break
except Exception as e: except Exception as e:
print(f"Error in browser health check: {e}") print(f"Error in browser health check: {e}")
await asyncio.sleep(60) # Wait before retrying # Continue the loop even if there's an error
await asyncio.sleep(30) # Wait a bit before retrying
async def force_cleanup_old_instances(): async def force_cleanup_old_instances():
"""Force cleanup old browser and page instances based on timeout""" """Force cleanup old browser and page instances based on timeout"""
@@ -266,58 +353,95 @@ async def force_cleanup_old_instances():
print(f"Force cleanup completed: {cleaned_browsers} browsers, {cleaned_pages} pages cleaned") print(f"Force cleanup completed: {cleaned_browsers} browsers, {cleaned_pages} pages cleaned")
async def force_cleanup_all_browsers(): async def force_cleanup_all_browsers():
"""Force cleanup all browsers - useful for emergency situations""" """Force cleanup all browsers - useful for emergency situations with improved efficiency"""
print("Force cleaning up all browsers...") print("Force cleaning up all browsers...")
try:
async with asyncio.timeout(20): # 20 second timeout for force cleanup
async with browser_lock: async with browser_lock:
# Clean up browsers in pool # Clean up browsers in pool
while not browser_pool.empty(): while not browser_pool.empty():
try: try:
browser = await browser_pool.get_nowait() browser = await asyncio.wait_for(browser_pool.get_nowait(), timeout=1.0)
await cleanup_browser(browser) await asyncio.wait_for(cleanup_browser(browser), timeout=5.0)
except asyncio.QueueEmpty: except (asyncio.TimeoutError, asyncio.QueueEmpty):
break break
except Exception as e:
print(f"Error cleaning up browser from pool: {e}")
# Clean up any remaining active browsers # Clean up any remaining active browsers
for browser in list(active_browsers): for browser in list(active_browsers):
try: try:
await cleanup_browser(browser) await asyncio.wait_for(cleanup_browser(browser), timeout=5.0)
except Exception as e: except (asyncio.TimeoutError, Exception) as e:
print(f"Error force cleaning browser: {e}") print(f"Error force cleaning browser: {e}")
print("Force cleanup completed") print("Force cleanup completed")
except asyncio.TimeoutError:
print("Force cleanup timed out")
except Exception as e:
print(f"Error during force cleanup: {e}")
@app.on_event("startup") @app.on_event("startup")
async def init_browser_pool(): async def init_browser_pool():
"""Initialize the browser pool on startup""" """Initialize the browser pool on startup with improved error handling"""
print("Initializing browser pool...") print("Initializing browser pool...")
# Start browser health check task
asyncio.create_task(check_browser_health())
# Pre-populate pool with initial browsers
for _ in range(min(2, MAX_BROWSERS)):
try: try:
browser = await create_browser() # Start browser health check task
await browser_pool.put(browser) global browser_health_check_task
except Exception as e: browser_health_check_task = asyncio.create_task(check_browser_health())
print(f"Error creating initial browser: {e}")
# Pre-populate pool with initial browsers (reduced from 2 to 1)
for _ in range(min(1, MAX_BROWSERS)):
try:
browser = await asyncio.wait_for(create_browser(), timeout=30.0)
await asyncio.wait_for(browser_pool.put(browser), timeout=5.0)
print(f"Created initial browser {_ + 1}")
except (asyncio.TimeoutError, Exception) as e:
print(f"Error creating initial browser {_ + 1}: {e}")
break
print(f"Browser pool initialized with {browser_pool.qsize()} browsers") print(f"Browser pool initialized with {browser_pool.qsize()} browsers")
except Exception as e:
print(f"Error during browser pool initialization: {e}")
# Don't fail startup, but log the error
@app.on_event("shutdown") @app.on_event("shutdown")
async def cleanup_browser_pool(): async def cleanup_browser_pool():
"""Cleanup browser pool on shutdown""" """Cleanup browser pool on shutdown with improved error handling"""
print("Cleaning up browser pool...") print("Cleaning up browser pool...")
await force_cleanup_all_browsers()
try:
# Cancel health check task
global browser_health_check_task
if browser_health_check_task and not browser_health_check_task.done():
browser_health_check_task.cancel()
try:
await browser_health_check_task
except asyncio.CancelledError:
pass
# Force cleanup all browsers with timeout
await asyncio.wait_for(force_cleanup_all_browsers(), timeout=30.0)
# Stop playwright instance # Stop playwright instance
global playwright_instance global playwright_instance
if playwright_instance: if playwright_instance:
await playwright_instance.stop() try:
await asyncio.wait_for(playwright_instance.stop(), timeout=10.0)
except asyncio.TimeoutError:
print("Playwright stop timeout")
except Exception as e:
print(f"Error stopping playwright: {e}")
finally:
playwright_instance = None playwright_instance = None
print("Browser pool cleanup completed") print("Browser pool cleanup completed")
except asyncio.TimeoutError:
print("Browser pool cleanup timed out")
except Exception as e:
print(f"Error during browser pool cleanup: {e}")
def signal_handler(signum, frame): def signal_handler(signum, frame):
"""Handle shutdown signals""" """Handle shutdown signals"""
@@ -329,6 +453,21 @@ def signal_handler(signum, frame):
signal.signal(signal.SIGINT, signal_handler) signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler) signal.signal(signal.SIGTERM, signal_handler)
# Add memory monitoring
async def monitor_memory_usage():
"""Monitor memory usage and trigger cleanup if needed"""
try:
memory = psutil.virtual_memory()
if memory.percent > 85: # If memory usage is above 85%
print(f"High memory usage detected: {memory.percent}%")
# Force garbage collection
gc.collect()
# Force cleanup old instances
await force_cleanup_old_instances()
print("Memory cleanup completed")
except Exception as e:
print(f"Error monitoring memory: {e}")
def init_db(): def init_db():
"""Initialize the SQLite database""" """Initialize the SQLite database"""
conn = sqlite3.connect('/db/cache.db') conn = sqlite3.connect('/db/cache.db')
@@ -363,9 +502,9 @@ def init_db():
print("Database initialized") print("Database initialized")
def get_cached_data(url, route): def get_cached_data(url, route):
"""Get cached data for a URL and route""" """Get cached data for a URL and route with improved error handling"""
try: try:
conn = sqlite3.connect('/db/cache.db') conn = sqlite3.connect('/db/cache.db', timeout=10.0) # Add timeout
cursor = conn.cursor() cursor = conn.cursor()
cursor.execute(''' cursor.execute('''
@@ -384,24 +523,36 @@ def get_cached_data(url, route):
if datetime.now() - created_time < timedelta(hours=CACHE_EXPIRY_HOURS): if datetime.now() - created_time < timedelta(hours=CACHE_EXPIRY_HOURS):
return json.loads(data) return json.loads(data)
return None
except sqlite3.OperationalError as e:
print(f"Database operational error getting cached data: {e}")
return None return None
except Exception as e: except Exception as e:
print(f"Error getting cached data: {e}") print(f"Error getting cached data: {e}")
return None return None
def save_to_cache(url, route, data): def save_to_cache(url, route, data):
"""Save data to cache""" """Save data to cache with improved error handling"""
try: try:
conn = sqlite3.connect('/db/cache.db') conn = sqlite3.connect('/db/cache.db', timeout=10.0) # Add timeout
cursor = conn.cursor() cursor = conn.cursor()
# Limit data size to prevent memory issues
data_str = json.dumps(data)
if len(data_str) > 10 * 1024 * 1024: # 10MB limit
print(f"Data too large for cache: {len(data_str)} bytes")
conn.close()
return
cursor.execute(''' cursor.execute('''
INSERT OR REPLACE INTO cache (url, route, data, created_at) INSERT OR REPLACE INTO cache (url, route, data, created_at)
VALUES (?, ?, ?, CURRENT_TIMESTAMP) VALUES (?, ?, ?, CURRENT_TIMESTAMP)
''', (url, route, json.dumps(data))) ''', (url, route, data_str))
conn.commit() conn.commit()
conn.close() conn.close()
except sqlite3.OperationalError as e:
print(f"Database operational error saving to cache: {e}")
except Exception as e: except Exception as e:
print(f"Error saving to cache: {e}") print(f"Error saving to cache: {e}")
@@ -458,46 +609,60 @@ async def health_check():
return {"status": "healthy"} return {"status": "healthy"}
async def safe_browser_operation(url, operation_func): async def safe_browser_operation(url, operation_func):
"""Safely perform a browser operation with proper resource management""" """Safely perform a browser operation with proper resource management and timeouts"""
async with operation_semaphore: async with operation_semaphore:
browser = None browser = None
context = None context = None
page = None page = None
try: try:
# Get browser from pool or create new one # Get browser from pool or create new one with timeout
try: try:
browser = await asyncio.wait_for(browser_pool.get(), timeout=10.0) browser = await asyncio.wait_for(browser_pool.get(), timeout=5.0)
except asyncio.TimeoutError: except asyncio.TimeoutError:
print("Timeout getting browser from pool, creating new one") print("Timeout getting browser from pool, creating new one")
browser = await create_browser() browser = await asyncio.wait_for(create_browser(), timeout=30.0)
# Create stealth context with Cloudflare bypass # Create stealth context with Cloudflare bypass
context = await CloudflareBypass.setup_stealth_context(browser) context = await asyncio.wait_for(
CloudflareBypass.setup_stealth_context(browser),
timeout=10.0
)
page = await context.new_page() page = await asyncio.wait_for(context.new_page(), timeout=10.0)
page.set_default_timeout(PAGE_TIMEOUT * 1000) # Convert to milliseconds
# Track page creation time for force cleanup # Track page creation time for force cleanup
page_creation_times[page] = time.time() page_creation_times[page] = time.time()
active_pages.add(page) active_pages.add(page)
# Setup stealth page with additional measures # Setup stealth page with additional measures
await CloudflareBypass.setup_stealth_page(page) await asyncio.wait_for(
CloudflareBypass.setup_stealth_page(page),
timeout=5.0
)
# Set up request interception for better performance (but allow essential resources) # Set up optimized request interception for better performance
await page.route("**/*", lambda route: route.abort() await page.route("**/*", lambda route: route.abort()
if route.request.resource_type in ['image', 'stylesheet', 'font', 'media'] if route.request.resource_type in ['image', 'stylesheet', 'font', 'media', 'script']
and not route.request.url.startswith('data:')
else route.continue_()) else route.continue_())
# Perform the operation # Perform the operation with timeout
result = await operation_func(page) result = await asyncio.wait_for(
operation_func(page),
timeout=BROWSER_OPERATION_TIMEOUT
)
return result return result
except asyncio.TimeoutError as e:
print(f"Browser operation timeout for {url}: {e}")
raise HTTPException(status_code=408, detail=f"Operation timeout: {str(e)}")
except Exception as e: except Exception as e:
print(f"Error in browser operation: {e}") print(f"Error in browser operation for {url}: {e}")
raise raise
finally: finally:
# Cleanup # Cleanup with timeouts
if page: if page:
try: try:
# Remove from tracking # Remove from tracking
@@ -505,21 +670,36 @@ async def safe_browser_operation(url, operation_func):
active_pages.remove(page) active_pages.remove(page)
if page in page_creation_times: if page in page_creation_times:
del page_creation_times[page] del page_creation_times[page]
await page.close() await asyncio.wait_for(page.close(), timeout=5.0)
except (asyncio.TimeoutError, Exception):
try:
await page.close(force=True)
except: except:
pass pass
if context: if context:
try: try:
await context.close() await asyncio.wait_for(context.close(), timeout=5.0)
except: except (asyncio.TimeoutError, Exception):
pass pass
if browser: if browser:
try: try:
# Return browser to pool if it's still healthy # Return browser to pool if it's still healthy and pool isn't full
if not browser_pool.full(): if not browser_pool.full():
await browser_pool.put(browser) # Quick health check before returning to pool
try:
contexts = browser.contexts
if contexts:
await asyncio.wait_for(browser_pool.put(browser), timeout=2.0)
else: else:
await cleanup_browser(browser) await cleanup_browser(browser)
except (asyncio.TimeoutError, Exception):
await cleanup_browser(browser)
else:
await cleanup_browser(browser)
except Exception as e:
print(f"Error returning browser to pool: {e}")
try:
await cleanup_browser(browser)
except: except:
pass pass
@@ -832,8 +1012,13 @@ async def system_status(x_api_key: Optional[str] = Header(None)):
"cpu_percent": cpu_percent, "cpu_percent": cpu_percent,
"memory_percent": memory.percent, "memory_percent": memory.percent,
"memory_available_gb": round(memory.available / (1024**3), 2), "memory_available_gb": round(memory.available / (1024**3), 2),
"memory_used_gb": round(memory.used / (1024**3), 2),
"memory_free_gb": round(memory.free / (1024**3), 2),
"memory_total_gb": round(memory.total / (1024**3), 2),
"disk_percent": disk.percent, "disk_percent": disk.percent,
"disk_free_gb": round(disk.free / (1024**3), 2) "disk_free_gb": round(disk.free / (1024**3), 2),
"disk_used_gb": round(disk.used / (1024**3), 2),
"disk_total_gb": round(disk.total / (1024**3), 2)
}, },
"browser_pool": { "browser_pool": {
"pool_size": pool_size, "pool_size": pool_size,
@@ -841,7 +1026,11 @@ async def system_status(x_api_key: Optional[str] = Header(None)):
"active_browsers": active_browser_count, "active_browsers": active_browser_count,
"active_pages": active_page_count, "active_pages": active_page_count,
"browser_ttl_seconds": BROWSER_TTL, "browser_ttl_seconds": BROWSER_TTL,
"instance_timeout_minutes": BROWSER_INSTANCE_TIMEOUT_MINUTES "instance_timeout_minutes": BROWSER_INSTANCE_TIMEOUT_MINUTES,
"max_concurrent_operations": MAX_CONCURRENT_OPERATIONS,
"browser_operation_timeout_seconds": BROWSER_OPERATION_TIMEOUT,
"page_timeout_seconds": PAGE_TIMEOUT,
"pool_health": "healthy" if pool_size > 0 and active_browser_count <= MAX_BROWSERS else "warning"
}, },
"cache": { "cache": {
"total_entries": cache_count, "total_entries": cache_count,
@@ -850,6 +1039,10 @@ async def system_status(x_api_key: Optional[str] = Header(None)):
"rate_limiting": { "rate_limiting": {
"requests_per_minute": RATE_LIMIT_MINUTE, "requests_per_minute": RATE_LIMIT_MINUTE,
"window_seconds": RATE_LIMIT_WINDOW "window_seconds": RATE_LIMIT_WINDOW
},
"performance": {
"memory_pressure": "high" if memory.percent > 85 else "normal" if memory.percent > 70 else "low",
"cpu_pressure": "high" if cpu_percent > 80 else "normal" if cpu_percent > 50 else "low"
} }
} }
except Exception as e: except Exception as e: