cache tweaks
Build and Push Docker Images / build-and-push (push) Successful in 32s

This commit is contained in:
2025-05-01 13:14:08 +02:00
parent c3fd4b3121
commit 5fd11399b6
3 changed files with 136 additions and 179 deletions
+2
View File
@@ -36,6 +36,8 @@ COPY . .
# Set environment variables # Set environment variables
ENV PYTHONUNBUFFERED=1 ENV PYTHONUNBUFFERED=1
ENV API_KEY="" ENV API_KEY=""
ENV CACHE_EXPIRY_HOURS=36
ENV CLEANUP_CRON="0 3 * * *"
# Create a non-root user and switch to it # Create a non-root user and switch to it
RUN useradd -m puppeteer && chown -R puppeteer:puppeteer /app RUN useradd -m puppeteer && chown -R puppeteer:puppeteer /app
+132 -178
View File
@@ -9,6 +9,8 @@ import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional, Dict, List, Any 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.triggers.cron import CronTrigger
app = FastAPI() app = FastAPI()
@@ -17,6 +19,12 @@ API_KEY = os.getenv('API_KEY')
if not API_KEY: if not API_KEY:
raise ValueError("API_KEY environment variable must be set") raise ValueError("API_KEY environment variable must be set")
# Get cache expiry time from environment variable (default: 36 hours)
CACHE_EXPIRY_HOURS = int(os.getenv('CACHE_EXPIRY_HOURS', '36'))
# Get cleanup cron schedule from environment variable (default: every day at 3 AM)
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'
@@ -104,11 +112,11 @@ def init_db():
# Define the database path # Define the database path
DB_PATH = '/db/cache.db' DB_PATH = '/db/cache.db'
# Get cached data if it exists and is not older than 36 hours # Get cached data if it exists and is not older than the expiry time
def get_cached_data(url, route): def get_cached_data(url, route):
conn = sqlite3.connect(DB_PATH) conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor() cursor = conn.cursor()
cache_expiry = int(time.time()) - (36 * 60 * 60) # 36 hours in seconds cache_expiry = int(time.time()) - (CACHE_EXPIRY_HOURS * 60 * 60) # Convert hours to seconds
cursor.execute( cursor.execute(
"SELECT data FROM cache WHERE url = ? AND route = ? AND timestamp > ?", "SELECT data FROM cache WHERE url = ? AND route = ? AND timestamp > ?",
(url, route, cache_expiry) (url, route, cache_expiry)
@@ -138,9 +146,54 @@ def save_to_cache(url, route, data):
conn.close() conn.close()
print(f"Saved to cache: {url} on route {route}") print(f"Saved to cache: {url} on route {route}")
# Function to clean up old cache entries
def cleanup_old_cache_entries():
try:
print(f"Running scheduled cache cleanup (entries older than {CACHE_EXPIRY_HOURS} hours)")
conn = sqlite3.connect(DB_PATH)
cursor = conn.cursor()
# Calculate the timestamp for entries older than the expiry time
expiry_timestamp = int(time.time()) - (CACHE_EXPIRY_HOURS * 60 * 60)
# Get count of entries to be deleted
cursor.execute("SELECT COUNT(*) FROM cache WHERE timestamp < ?", (expiry_timestamp,))
count = cursor.fetchone()[0]
# Delete old entries
cursor.execute("DELETE FROM cache WHERE timestamp < ?", (expiry_timestamp,))
conn.commit()
conn.close()
print(f"Cache cleanup completed: {count} entries removed")
except Exception as e:
print(f"Error during cache cleanup: {e}")
# Initialize scheduler for periodic cache cleanup
scheduler = BackgroundScheduler()
scheduler.add_job(
cleanup_old_cache_entries,
CronTrigger.from_crontab(CLEANUP_CRON),
id='cache_cleanup_job',
replace_existing=True
)
# Initialize database on startup # Initialize database on startup
init_db() init_db()
# Start the scheduler when the application starts
@app.on_event("startup")
def start_scheduler():
scheduler.start()
print(f"Cache cleanup scheduler started with cron: {CLEANUP_CRON}")
print(f"Cache entries will expire after {CACHE_EXPIRY_HOURS} hours")
# Shutdown the scheduler when the application stops
@app.on_event("shutdown")
def shutdown_scheduler():
scheduler.shutdown(wait=False)
print("Cache cleanup scheduler stopped")
async def wait_for_network_idle(page): async def wait_for_network_idle(page):
"""Wait until no network requests are in flight""" """Wait until no network requests are in flight"""
await page.waitForNetworkIdle(idleTime=500, timeout=30000) await page.waitForNetworkIdle(idleTime=500, timeout=30000)
@@ -149,6 +202,43 @@ async def wait_for_network_idle(page):
async def health_check(): async def health_check():
return {"status": "ok"} return {"status": "ok"}
async def safe_browser_operation(url, operation_func):
"""Safely perform browser operations with proper cleanup"""
browser = None
try:
browser = await launch(
headless=True,
executablePath='/usr/bin/google-chrome',
args=['--no-sandbox', '--disable-setuid-sandbox'],
handleSIGINT=False,
handleSIGTERM=False,
handleSIGHUP=False
)
# Create new page with timeout
page = await browser.newPage()
page.setDefaultNavigationTimeout(30000)
# Set custom user agent
await page.setUserAgent(CUSTOM_USER_AGENT)
# Call the operation function that uses the page
result = await operation_func(page)
# Explicitly close the page
await page.close()
return result
finally:
# Ensure browser is closed properly
if browser:
try:
await browser.close()
except Exception as e:
print(f"Error closing browser: {e}")
# We don't re-raise here to avoid masking the original error
@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)):
# Validate API key # Validate API key
@@ -163,33 +253,30 @@ async def visit_url(url: str, x_api_key: Optional[str] = Header(None)):
if cached_result: if cached_result:
return cached_result return cached_result
browser = None
try: try:
print(decoded_url) print(f"Visiting URL: {decoded_url}")
# Launch browser using installed Chrome # Define the operation to perform with the browser
browser = await launch( async def visit_operation(page):
headless=True, try:
executablePath='/usr/bin/google-chrome', response = await page.goto(decoded_url, waitUntil='networkidle2', timeout=30000)
args=['--no-sandbox', '--disable-setuid-sandbox'] if not response:
) print(f"Warning: No response object returned for {decoded_url}")
# Create new page # Get page content
page = await browser.newPage() content = await page.content()
return {"status": "success", "content": content}
except Exception as e:
print(f"Error during page navigation: {e}")
# Try to get content anyway
try:
content = await page.content()
return {"status": "partial", "content": content, "error": str(e)}
except:
raise HTTPException(status_code=500, detail=f"Failed to get page content: {str(e)}")
# Set custom user agent # Perform the operation
await page.setUserAgent(CUSTOM_USER_AGENT) result = await safe_browser_operation(decoded_url, visit_operation)
# Navigate to URL and wait for network idle
await page.goto(decoded_url, waitUntil='networkidle2')
# Get page content
content = await page.content()
# Close page
await page.close()
result = {"status": "success", "content": content}
# Save to cache # Save to cache
save_to_cache(decoded_url, "visit", result) save_to_cache(decoded_url, "visit", result)
@@ -197,13 +284,9 @@ async def visit_url(url: str, x_api_key: Optional[str] = Header(None)):
return result return result
except Exception as e: except Exception as e:
print(f"Error visiting URL {decoded_url}: {e}")
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
finally:
# Ensure browser is closed even if an error occurs
if browser:
await browser.close()
@app.get("/seo") @app.get("/seo")
async def extract_seo(url: str, x_api_key: Optional[str] = Header(None)): async def extract_seo(url: str, x_api_key: Optional[str] = Header(None)):
"""Extract SEO information from a website""" """Extract SEO information from a website"""
@@ -219,112 +302,17 @@ async def extract_seo(url: str, x_api_key: Optional[str] = Header(None)):
if cached_result: if cached_result:
return cached_result return cached_result
browser = None
try: try:
print(f"Extracting SEO from: {decoded_url}") print(f"Extracting SEO from: {decoded_url}")
# Launch browser using installed Chrome # Use the safe_browser_operation function to perform the browser operation
browser = await launch( result = await safe_browser_operation(decoded_url, asyncio.run)
headless=True,
executablePath='/usr/bin/google-chrome',
args=['--no-sandbox', '--disable-setuid-sandbox']
)
# Create new page
page = await browser.newPage()
# Set custom user agent
await page.setUserAgent(CUSTOM_USER_AGENT)
# Navigate to URL and wait for network idle
await page.goto(decoded_url, waitUntil='networkidle2')
# Extract JSON-LD data
json_ld_data = await page.evaluate('''() => {
const jsonLdScripts = Array.from(document.querySelectorAll('script[type="application/ld+json"]'));
return jsonLdScripts.map(script => {
try {
return JSON.parse(script.textContent);
} catch (e) {
return { error: "Failed to parse JSON-LD", content: script.textContent };
}
});
}''')
# Extract meta tags
meta_tags = await page.evaluate('''() => {
const metaTags = Array.from(document.querySelectorAll('meta'));
return metaTags.map(tag => {
const attributes = Array.from(tag.attributes);
const result = {};
attributes.forEach(attr => {
result[attr.name] = attr.value;
});
return result;
});
}''')
# Extract title
title = await page.evaluate('document.title')
# Extract canonical URL
canonical_url = await page.evaluate('''() => {
const link = document.querySelector('link[rel="canonical"]');
return link ? link.href : null;
}''')
# Extract Open Graph data
og_data = await page.evaluate('''() => {
const ogTags = Array.from(document.querySelectorAll('meta[property^="og:"]'));
const result = {};
ogTags.forEach(tag => {
const property = tag.getAttribute('property');
const content = tag.getAttribute('content');
result[property] = content;
});
return result;
}''')
# Extract Twitter Card data
twitter_data = await page.evaluate('''() => {
const twitterTags = Array.from(document.querySelectorAll('meta[name^="twitter:"]'));
const result = {};
twitterTags.forEach(tag => {
const name = tag.getAttribute('name');
const content = tag.getAttribute('content');
result[name] = content;
});
return result;
}''')
# Close page
await page.close()
# Compile all SEO data
seo_data = {
"title": title,
"canonical_url": canonical_url,
"json_ld": json_ld_data,
"meta_tags": meta_tags,
"open_graph": og_data,
"twitter_card": twitter_data
}
result = {"status": "success", "seo_data": seo_data}
# Save to cache
save_to_cache(decoded_url, "seo", result)
return result return result
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
finally:
# Ensure browser is closed even if an error occurs
if browser:
await browser.close()
@app.get("/meta") @app.get("/meta")
async def extract_meta_tags(url: str, x_api_key: Optional[str] = Header(None)): async def extract_meta_tags(url: str, x_api_key: Optional[str] = Header(None)):
"""Extract meta tags from a website""" """Extract meta tags from a website"""
@@ -340,65 +328,17 @@ async def extract_meta_tags(url: str, x_api_key: Optional[str] = Header(None)):
if cached_result: if cached_result:
return cached_result return cached_result
browser = None
try: try:
print(f"Extracting meta tags from: {decoded_url}") print(f"Extracting meta tags from: {decoded_url}")
# Launch browser using installed Chrome # Use the safe_browser_operation function to perform the browser operation
browser = await launch( result = await safe_browser_operation(decoded_url, asyncio.run)
headless=True,
executablePath='/usr/bin/google-chrome',
args=['--no-sandbox', '--disable-setuid-sandbox']
)
# Create new page
page = await browser.newPage()
# Set custom user agent
await page.setUserAgent(CUSTOM_USER_AGENT)
# Navigate to URL and wait for network idle
await page.goto(decoded_url, waitUntil='networkidle2')
# Extract meta tags
meta_tags = await page.evaluate('''() => {
const metaTags = Array.from(document.querySelectorAll('meta'));
return metaTags.map(tag => {
const attributes = Array.from(tag.attributes);
const result = {};
attributes.forEach(attr => {
result[attr.name] = attr.value;
});
return result;
});
}''')
# Extract title
title = await page.evaluate('document.title')
# Close page
await page.close()
result = {
"status": "success",
"url": decoded_url,
"title": title,
"meta_tags": meta_tags
}
# Save to cache
save_to_cache(decoded_url, "meta", result)
return result return result
except Exception as e: except Exception as e:
raise HTTPException(status_code=500, detail=str(e)) raise HTTPException(status_code=500, detail=str(e))
finally:
# Ensure browser is closed even if an error occurs
if browser:
await browser.close()
@app.get("/cache/clear") @app.get("/cache/clear")
async def clear_cache(x_api_key: Optional[str] = Header(None)): async def clear_cache(x_api_key: Optional[str] = Header(None)):
"""Clear the entire cache database""" """Clear the entire cache database"""
@@ -437,6 +377,16 @@ async def cache_stats(x_api_key: Optional[str] = Header(None)):
cursor.execute("SELECT COUNT(*) FROM cache WHERE timestamp > ?", (recent_timestamp,)) cursor.execute("SELECT COUNT(*) FROM cache WHERE timestamp > ?", (recent_timestamp,))
recent_entries = cursor.fetchone()[0] recent_entries = cursor.fetchone()[0]
# Get oldest entry timestamp
cursor.execute("SELECT MIN(timestamp) FROM cache")
oldest_timestamp = cursor.fetchone()[0]
oldest_date = datetime.fromtimestamp(oldest_timestamp).isoformat() if oldest_timestamp else None
# Get newest entry timestamp
cursor.execute("SELECT MAX(timestamp) FROM cache")
newest_timestamp = cursor.fetchone()[0]
newest_date = datetime.fromtimestamp(newest_timestamp).isoformat() if newest_timestamp else None
conn.close() conn.close()
return { return {
@@ -444,7 +394,11 @@ async def cache_stats(x_api_key: Optional[str] = Header(None)):
"stats": { "stats": {
"total_entries": total_entries, "total_entries": total_entries,
"entries_by_route": routes, "entries_by_route": routes,
"recent_entries": recent_entries "recent_entries": recent_entries,
"oldest_entry": oldest_date,
"newest_entry": newest_date,
"cache_expiry_hours": CACHE_EXPIRY_HOURS,
"cleanup_schedule": CLEANUP_CRON
} }
} }
+1
View File
@@ -2,3 +2,4 @@ fastapi==0.68.1
uvicorn==0.15.0 uvicorn==0.15.0
pyppeteer==1.0.2 pyppeteer==1.0.2
psutil==6.0.0 psutil==6.0.0
apscheduler