feat: optimize TTFB with connection pooling, caching, and performance monitoring

This commit is contained in:
Cipher Vance 2025-08-25 14:00:56 -05:00
parent 55f22998b3
commit 45a1b2f234

271
server.py
View file

@ -1,11 +1,12 @@
import os import os
import time import time
import logging
from threading import Thread from threading import Thread
import smtplib import smtplib
from email.mime.text import MIMEText from email.mime.text import MIMEText
from flask import Flask, render_template, request, jsonify from flask import Flask, render_template, request, jsonify, g
from dotenv import load_dotenv from dotenv import load_dotenv
from database import init_db, get_connection, add_email, remove_email from database import init_db, get_connection, return_connection, add_email, remove_email
load_dotenv() load_dotenv()
@ -16,18 +17,14 @@ SMTP_PASSWORD = os.getenv('SMTP_PASSWORD')
app = Flask(__name__) app = Flask(__name__)
_db_initialized = False # Configure logging
logging.basicConfig(level=logging.INFO)
# Cache configuration
_newsletter_cache = {} _newsletter_cache = {}
_cache_timestamp = {} _cache_timestamp = {}
CACHE_DURATION = 300 CACHE_DURATION = 300
def ensure_db_initialized():
"""Lazy database initialization - only runs on first database access"""
global _db_initialized
if not _db_initialized:
init_db()
_db_initialized = True
def get_newsletters_cached(): def get_newsletters_cached():
"""Get newsletters with caching to reduce database hits""" """Get newsletters with caching to reduce database hits"""
current_time = time.time() current_time = time.time()
@ -36,26 +33,32 @@ def get_newsletters_cached():
current_time - _cache_timestamp.get('newsletters', 0) < CACHE_DURATION): current_time - _cache_timestamp.get('newsletters', 0) < CACHE_DURATION):
return _newsletter_cache['newsletters'] return _newsletter_cache['newsletters']
ensure_db_initialized() conn = None
conn = get_connection() try:
cursor = conn.cursor() conn = get_connection()
cursor.execute( cursor = conn.cursor()
"SELECT id, subject, body, sent_at " cursor.execute(
"FROM newsletters ORDER BY sent_at DESC" "SELECT id, subject, body, sent_at "
) "FROM newsletters ORDER BY sent_at DESC LIMIT 100"
rows = cursor.fetchall() )
cursor.close() rows = cursor.fetchall()
conn.close() cursor.close()
newsletters = [ newsletters = [
{"id": r[0], "subject": r[1], "body": r[2], "sent_at": r[3]} {"id": r[0], "subject": r[1], "body": r[2], "sent_at": r[3]}
for r in rows for r in rows
] ]
_newsletter_cache['newsletters'] = newsletters _newsletter_cache['newsletters'] = newsletters
_cache_timestamp['newsletters'] = current_time _cache_timestamp['newsletters'] = current_time
return newsletters return newsletters
except Exception as e:
app.logger.error(f"Database error in get_newsletters_cached: {e}")
return []
finally:
if conn:
return_connection(conn)
def get_newsletter_by_id_cached(newsletter_id): def get_newsletter_by_id_cached(newsletter_id):
"""Get single newsletter with caching""" """Get single newsletter with caching"""
@ -66,32 +69,38 @@ def get_newsletter_by_id_cached(newsletter_id):
current_time - _cache_timestamp.get(cache_key, 0) < CACHE_DURATION): current_time - _cache_timestamp.get(cache_key, 0) < CACHE_DURATION):
return _newsletter_cache[cache_key] return _newsletter_cache[cache_key]
ensure_db_initialized() conn = None
conn = get_connection() try:
cursor = conn.cursor() conn = get_connection()
cursor.execute( cursor = conn.cursor()
"SELECT id, subject, body, sent_at " cursor.execute(
"FROM newsletters WHERE id = %s", "SELECT id, subject, body, sent_at "
(newsletter_id,) "FROM newsletters WHERE id = %s",
) (newsletter_id,)
row = cursor.fetchone() )
cursor.close() row = cursor.fetchone()
conn.close() cursor.close()
if not row:
return None
newsletter = { if not row:
"id": row[0], return None
"subject": row[1],
"body": row[2], newsletter = {
"sent_at": row[3] "id": row[0],
} "subject": row[1],
"body": row[2],
_newsletter_cache[cache_key] = newsletter "sent_at": row[3]
_cache_timestamp[cache_key] = current_time }
return newsletter _newsletter_cache[cache_key] = newsletter
_cache_timestamp[cache_key] = current_time
return newsletter
except Exception as e:
app.logger.error(f"Database error in get_newsletter_by_id_cached: {e}")
return None
finally:
if conn:
return_connection(conn)
def clear_newsletter_cache(): def clear_newsletter_cache():
"""Clear newsletter cache when data is updated""" """Clear newsletter cache when data is updated"""
@ -103,35 +112,49 @@ def clear_newsletter_cache():
_cache_timestamp.pop(key, None) _cache_timestamp.pop(key, None)
@app.before_request @app.before_request
def start_timer(): def before_request():
request._start_time = time.time() """Start timing the request and set up request context"""
g.start_time = time.time()
@app.after_request @app.after_request
def log_request(response): def after_request(response):
elapsed = time.time() - getattr(request, '_start_time', time.time()) """Log request timing and performance metrics"""
app.logger.info(f"{request.method} {request.path} completed in {elapsed:.3f}s") total_time = time.time() - g.start_time
# Log slow requests
if total_time > 1.0:
app.logger.warning(f"Slow request: {request.method} {request.path} took {total_time:.3f}s")
elif total_time > 0.5:
app.logger.info(f"Request: {request.method} {request.path} took {total_time:.3f}s")
# Add performance headers for debugging
response.headers['X-Response-Time'] = f"{total_time:.3f}s"
return response return response
def send_confirmation_email(to_address: str, unsubscribe_link: str): def send_confirmation_email(to_address: str, unsubscribe_link: str):
""" """
Sends the HTML confirmation email to `to_address`. Sends the HTML confirmation email to `to_address`.
This runs inside its own SMTP_SSL connection (timeout=10s). This runs inside its own SMTP_SSL connection with reduced timeout.
""" """
subject = "Thanks for subscribing!"
html_body = render_template(
"confirmation_email.html",
unsubscribe_link=unsubscribe_link
)
msg = MIMEText(html_body, "html", "utf-8")
msg["Subject"] = subject
msg["From"] = SMTP_USER
msg["To"] = to_address
try: try:
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10) as server: subject = "Thanks for subscribing!"
html_body = render_template(
"confirmation_email.html",
unsubscribe_link=unsubscribe_link
)
msg = MIMEText(html_body, "html", "utf-8")
msg["Subject"] = subject
msg["From"] = SMTP_USER
msg["To"] = to_address
with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=5) as server:
server.login(SMTP_USER, SMTP_PASSWORD) server.login(SMTP_USER, SMTP_PASSWORD)
server.sendmail(SMTP_USER, [to_address], msg.as_string()) server.sendmail(SMTP_USER, [to_address], msg.as_string())
app.logger.info(f"Confirmation email sent successfully to {to_address}")
except Exception as e: except Exception as e:
app.logger.error(f"Failed to send email to {to_address}: {e}") app.logger.error(f"Failed to send email to {to_address}: {e}")
@ -148,65 +171,113 @@ def index():
@app.route("/subscribe", methods=["POST"]) @app.route("/subscribe", methods=["POST"])
def subscribe(): def subscribe():
"""Subscribe endpoint - lazy loads database only when needed""" """Subscribe endpoint with optimized database handling"""
data = request.get_json() or {} data = request.get_json() or {}
email = data.get("email") email = data.get("email")
if not email: if not email:
return jsonify(error="No email provided"), 400 return jsonify(error="No email provided"), 400
ensure_db_initialized() # Validate email format (basic check)
if "@" not in email or "." not in email.split("@")[-1]:
return jsonify(error="Invalid email format"), 400
if add_email(email): try:
unsubscribe_link = f"{request.url_root}unsubscribe?email={email}" if add_email(email):
unsubscribe_link = f"{request.url_root}unsubscribe?email={email}"
Thread( # Start email sending in background thread
target=send_confirmation_async, Thread(
args=(email, unsubscribe_link), target=send_confirmation_async,
daemon=True args=(email, unsubscribe_link),
).start() daemon=True
).start()
return jsonify(message="Email has been added"), 201 return jsonify(message="Email has been added"), 201
else:
return jsonify(error="Email already exists"), 400 return jsonify(error="Email already exists"), 400
except Exception as e:
app.logger.error(f"Error in subscribe endpoint: {e}")
return jsonify(error="Internal server error"), 500
@app.route("/unsubscribe", methods=["GET"]) @app.route("/unsubscribe", methods=["GET"])
def unsubscribe(): def unsubscribe():
"""Unsubscribe endpoint - lazy loads database only when needed""" """Unsubscribe endpoint with optimized database handling"""
email = request.args.get("email") email = request.args.get("email")
if not email: if not email:
return "No email specified.", 400 return "No email specified.", 400
ensure_db_initialized() try:
if remove_email(email):
if remove_email(email): return f"The email {email} has been unsubscribed.", 200
return f"The email {email} has been unsubscribed.", 200 else:
return f"Email {email} was not found or has already been unsubscribed.", 400 return f"Email {email} was not found or has already been unsubscribed.", 400
except Exception as e:
app.logger.error(f"Error in unsubscribe endpoint: {e}")
return "Internal server error", 500
@app.route("/newsletters", methods=["GET"]) @app.route("/newsletters", methods=["GET"])
def newsletters(): def newsletters():
""" """
List all newsletters (newest first) with caching for better performance. List all newsletters (newest first) with caching for better performance.
""" """
newsletters = get_newsletters_cached() try:
return render_template("newsletters.html", newsletters=newsletters) newsletters = get_newsletters_cached()
return render_template("newsletters.html", newsletters=newsletters)
except Exception as e:
app.logger.error(f"Error in newsletters endpoint: {e}")
return "Internal server error", 500
@app.route("/newsletter/<int:newsletter_id>", methods=["GET"]) @app.route("/newsletter/<int:newsletter_id>", methods=["GET"])
def newsletter_detail(newsletter_id): def newsletter_detail(newsletter_id):
""" """
Show a single newsletter by its ID with caching. Show a single newsletter by its ID with caching.
""" """
newsletter = get_newsletter_by_id_cached(newsletter_id) try:
newsletter = get_newsletter_by_id_cached(newsletter_id)
if not newsletter:
return "Newsletter not found.", 404 if not newsletter:
return "Newsletter not found.", 404
return render_template("newsletter_detail.html", newsletter=newsletter) return render_template("newsletter_detail.html", newsletter=newsletter)
except Exception as e:
app.logger.error(f"Error in newsletter_detail endpoint: {e}")
return "Internal server error", 500
@app.route("/admin/clear-cache", methods=["POST"]) @app.route("/admin/clear-cache", methods=["POST"])
def clear_cache(): def clear_cache():
"""Admin endpoint to clear newsletter cache""" """Admin endpoint to clear newsletter cache"""
clear_newsletter_cache() try:
return jsonify(message="Cache cleared successfully"), 200 clear_newsletter_cache()
return jsonify(message="Cache cleared successfully"), 200
except Exception as e:
app.logger.error(f"Error clearing cache: {e}")
return jsonify(error="Failed to clear cache"), 500
@app.route("/health", methods=["GET"])
def health_check():
"""Health check endpoint for monitoring"""
return jsonify(status="healthy", timestamp=time.time()), 200
# Error handlers
@app.errorhandler(404)
def not_found(error):
return jsonify(error="Not found"), 404
@app.errorhandler(500)
def internal_error(error):
return jsonify(error="Internal server error"), 500
# Initialize database at startup
try:
init_db()
app.logger.info("Database initialized successfully")
except Exception as e:
app.logger.error(f"Failed to initialize database: {e}")
raise
if __name__ == "__main__": if __name__ == "__main__":
app.run(host="0.0.0.0", debug=True) app.run(host="0.0.0.0", debug=True)