From 45a1b2f234b41f751f33d546c3944153a7424e9b Mon Sep 17 00:00:00 2001 From: Cipher Vance Date: Mon, 25 Aug 2025 14:00:56 -0500 Subject: [PATCH] feat: optimize TTFB with connection pooling, caching, and performance monitoring --- server.py | 271 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 171 insertions(+), 100 deletions(-) diff --git a/server.py b/server.py index 47d4f30..9889ea9 100644 --- a/server.py +++ b/server.py @@ -1,11 +1,12 @@ import os import time +import logging from threading import Thread import smtplib 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 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() @@ -16,18 +17,14 @@ SMTP_PASSWORD = os.getenv('SMTP_PASSWORD') app = Flask(__name__) -_db_initialized = False +# Configure logging +logging.basicConfig(level=logging.INFO) + +# Cache configuration _newsletter_cache = {} _cache_timestamp = {} 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(): """Get newsletters with caching to reduce database hits""" current_time = time.time() @@ -36,26 +33,32 @@ def get_newsletters_cached(): current_time - _cache_timestamp.get('newsletters', 0) < CACHE_DURATION): return _newsletter_cache['newsletters'] - ensure_db_initialized() - conn = get_connection() - cursor = conn.cursor() - cursor.execute( - "SELECT id, subject, body, sent_at " - "FROM newsletters ORDER BY sent_at DESC" - ) - rows = cursor.fetchall() - cursor.close() - conn.close() - - newsletters = [ - {"id": r[0], "subject": r[1], "body": r[2], "sent_at": r[3]} - for r in rows - ] - - _newsletter_cache['newsletters'] = newsletters - _cache_timestamp['newsletters'] = current_time - - return newsletters + conn = None + try: + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "SELECT id, subject, body, sent_at " + "FROM newsletters ORDER BY sent_at DESC LIMIT 100" + ) + rows = cursor.fetchall() + cursor.close() + + newsletters = [ + {"id": r[0], "subject": r[1], "body": r[2], "sent_at": r[3]} + for r in rows + ] + + _newsletter_cache['newsletters'] = newsletters + _cache_timestamp['newsletters'] = current_time + + 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): """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): return _newsletter_cache[cache_key] - ensure_db_initialized() - conn = get_connection() - cursor = conn.cursor() - cursor.execute( - "SELECT id, subject, body, sent_at " - "FROM newsletters WHERE id = %s", - (newsletter_id,) - ) - row = cursor.fetchone() - cursor.close() - conn.close() - - if not row: - return None + conn = None + try: + conn = get_connection() + cursor = conn.cursor() + cursor.execute( + "SELECT id, subject, body, sent_at " + "FROM newsletters WHERE id = %s", + (newsletter_id,) + ) + row = cursor.fetchone() + cursor.close() - newsletter = { - "id": row[0], - "subject": row[1], - "body": row[2], - "sent_at": row[3] - } - - _newsletter_cache[cache_key] = newsletter - _cache_timestamp[cache_key] = current_time - - return newsletter + if not row: + return None + + newsletter = { + "id": row[0], + "subject": row[1], + "body": row[2], + "sent_at": row[3] + } + + _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(): """Clear newsletter cache when data is updated""" @@ -103,35 +112,49 @@ def clear_newsletter_cache(): _cache_timestamp.pop(key, None) @app.before_request -def start_timer(): - request._start_time = time.time() +def before_request(): + """Start timing the request and set up request context""" + g.start_time = time.time() @app.after_request -def log_request(response): - elapsed = time.time() - getattr(request, '_start_time', time.time()) - app.logger.info(f"{request.method} {request.path} completed in {elapsed:.3f}s") +def after_request(response): + """Log request timing and performance metrics""" + 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 def send_confirmation_email(to_address: str, unsubscribe_link: str): """ 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: - 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.sendmail(SMTP_USER, [to_address], msg.as_string()) + + app.logger.info(f"Confirmation email sent successfully to {to_address}") + except Exception as e: app.logger.error(f"Failed to send email to {to_address}: {e}") @@ -148,65 +171,113 @@ def index(): @app.route("/subscribe", methods=["POST"]) def subscribe(): - """Subscribe endpoint - lazy loads database only when needed""" + """Subscribe endpoint with optimized database handling""" data = request.get_json() or {} email = data.get("email") + if not email: 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): - unsubscribe_link = f"{request.url_root}unsubscribe?email={email}" + try: + if add_email(email): + unsubscribe_link = f"{request.url_root}unsubscribe?email={email}" - Thread( - target=send_confirmation_async, - args=(email, unsubscribe_link), - daemon=True - ).start() + # Start email sending in background thread + Thread( + target=send_confirmation_async, + args=(email, unsubscribe_link), + daemon=True + ).start() - return jsonify(message="Email has been added"), 201 - - return jsonify(error="Email already exists"), 400 + return jsonify(message="Email has been added"), 201 + else: + 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"]) def unsubscribe(): - """Unsubscribe endpoint - lazy loads database only when needed""" + """Unsubscribe endpoint with optimized database handling""" email = request.args.get("email") + if not email: return "No email specified.", 400 - ensure_db_initialized() - - if remove_email(email): - return f"The email {email} has been unsubscribed.", 200 - return f"Email {email} was not found or has already been unsubscribed.", 400 + try: + if remove_email(email): + return f"The email {email} has been unsubscribed.", 200 + else: + 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"]) def newsletters(): """ List all newsletters (newest first) with caching for better performance. """ - newsletters = get_newsletters_cached() - return render_template("newsletters.html", newsletters=newsletters) + try: + 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/", methods=["GET"]) def newsletter_detail(newsletter_id): """ Show a single newsletter by its ID with caching. """ - newsletter = get_newsletter_by_id_cached(newsletter_id) - - if not newsletter: - return "Newsletter not found.", 404 + try: + newsletter = get_newsletter_by_id_cached(newsletter_id) + + 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"]) def clear_cache(): """Admin endpoint to clear newsletter cache""" - clear_newsletter_cache() - return jsonify(message="Cache cleared successfully"), 200 + try: + 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__": app.run(host="0.0.0.0", debug=True) \ No newline at end of file