Refactor email sending for non-blocking performance and add request timing
• Dockerfile: bump Gunicorn workers to 4 for concurrent request handling • server.py: - Load SMTP credentials once at startup - Wrap send_confirmation_email in a daemon Thread for “fire-and-forget” delivery - Replace print() with structured app.logger.error() on failure - Add before_request/after_request hooks to log per-request durations - Use context manager for SMTP_SSL connections (auto-close) - Simplify route JSON responses and HTTP methods declarations - Retain existing DB logic but recommend low DB connect_timeouts - Ensure Flask binds on 0.0.0.0 in development These changes eliminate request blocking on email I/O, expose request latencies in logs, and improve overall concurrency.
This commit is contained in:
		
							parent
							
								
									af910a291a
								
							
						
					
					
						commit
						6bf6d15fbe
					
				
					 2 changed files with 90 additions and 51 deletions
				
			
		|  | @ -14,5 +14,4 @@ ENV FLASK_APP=server.py | |||
| 
 | ||||
| EXPOSE 5000 | ||||
| 
 | ||||
| CMD ["gunicorn", "--bind", "0.0.0.0:5000", "server:app"] | ||||
| 
 | ||||
| CMD ["gunicorn", "--bind", "0.0.0.0:5000", "--workers", "4", "server:app"] | ||||
							
								
								
									
										136
									
								
								server.py
									
										
									
									
									
								
							
							
						
						
									
										136
									
								
								server.py
									
										
									
									
									
								
							|  | @ -1,59 +1,84 @@ | |||
| import os | ||||
| import time | ||||
| from threading import Thread | ||||
| import smtplib | ||||
| from email.mime.text import MIMEText | ||||
| from flask import Flask, render_template, request, jsonify | ||||
| from database import get_connection, init_db, add_email, remove_email | ||||
| from dotenv import load_dotenv | ||||
| from collections import namedtuple | ||||
| from database import init_db, get_connection, add_email, remove_email | ||||
| 
 | ||||
| load_dotenv() | ||||
| 
 | ||||
| SMTP_SERVER   = os.getenv('SMTP_SERVER') | ||||
| SMTP_PORT     = int(os.getenv('SMTP_PORT', 465)) | ||||
| SMTP_USER     = os.getenv('SMTP_USER') | ||||
| SMTP_PASSWORD = os.getenv('SMTP_PASSWORD') | ||||
| 
 | ||||
| app = Flask(__name__) | ||||
| init_db() | ||||
| 
 | ||||
| def send_confirmation_email(email): | ||||
|     SMTP_SERVER = os.getenv('SMTP_SERVER') | ||||
|     SMTP_PORT = int(os.getenv('SMTP_PORT', 465)) | ||||
|     SMTP_USER = os.getenv('SMTP_USER') | ||||
|     SMTP_PASSWORD = os.getenv('SMTP_PASSWORD') | ||||
| @app.before_request | ||||
| def start_timer(): | ||||
|     request._start_time = time.time() | ||||
| 
 | ||||
|     unsubscribe_link = f"{request.url_root}unsubscribe?email={email}" | ||||
| 
 | ||||
|     subject = 'Thanks for subscribing!' | ||||
| @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") | ||||
|     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). | ||||
|     """ | ||||
|     subject = "Thanks for subscribing!" | ||||
|     html_body = render_template( | ||||
|         'confirmation_email.html', | ||||
|         "confirmation_email.html", | ||||
|         unsubscribe_link=unsubscribe_link | ||||
|     ) | ||||
| 
 | ||||
|     msg = MIMEText(html_body, 'html', 'utf-8')  # Specify HTML | ||||
|     msg['Subject'] = subject | ||||
|     msg['From'] = SMTP_USER | ||||
|     msg['To'] = email | ||||
|     msg = MIMEText(html_body, "html", "utf-8") | ||||
|     msg["Subject"] = subject | ||||
|     msg["From"]    = SMTP_USER | ||||
|     msg["To"]      = to_address | ||||
| 
 | ||||
|     try: | ||||
|         server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10) | ||||
|         server.login(SMTP_USER, SMTP_PASSWORD) | ||||
|         server.sendmail(SMTP_USER, email, msg.as_string()) | ||||
|         server.quit() | ||||
|         with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10) as server: | ||||
|             server.login(SMTP_USER, SMTP_PASSWORD) | ||||
|             server.sendmail(SMTP_USER, [to_address], msg.as_string()) | ||||
|     except Exception as e: | ||||
|         print(f"Failed to send email to {email}: {e}") | ||||
|         app.logger.error(f"Failed to send email to {to_address}: {e}") | ||||
| 
 | ||||
| @app.route("/") | ||||
| def send_confirmation_async(email, unsubscribe_link): | ||||
|     """ | ||||
|     Wrapper for threading.Thread target. | ||||
|     """ | ||||
|     send_confirmation_email(email, unsubscribe_link) | ||||
| 
 | ||||
| @app.route("/", methods=["GET"]) | ||||
| def index(): | ||||
|     return render_template("index.html") | ||||
| 
 | ||||
| @app.route("/subscribe", methods=["POST"]) | ||||
| def subscribe(): | ||||
|     data = request.get_json() | ||||
|     email = data.get('email') | ||||
|     data = request.get_json() or {} | ||||
|     email = data.get("email") | ||||
|     if not email: | ||||
|         return jsonify({"error": "No email provided"}), 400 | ||||
|         return jsonify(error="No email provided"), 400 | ||||
| 
 | ||||
|     if add_email(email): | ||||
|         send_confirmation_email(email) | ||||
|         return jsonify({"message": "Email has been added"}), 201 | ||||
|     else: | ||||
|         return jsonify({"error": "Email already exists"}), 400 | ||||
|         unsubscribe_link = f"{request.url_root}unsubscribe?email={email}" | ||||
| 
 | ||||
|         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 | ||||
| 
 | ||||
| @app.route("/unsubscribe", methods=["GET"]) | ||||
| def unsubscribe(): | ||||
|  | @ -63,42 +88,57 @@ def unsubscribe(): | |||
| 
 | ||||
|     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 | ||||
|     return f"Email {email} was not found or has already been unsubscribed.", 400 | ||||
| 
 | ||||
| @app.route("/newsletters") | ||||
| 
 | ||||
| @app.route("/newsletters", methods=["GET"]) | ||||
| def newsletters(): | ||||
|     conn = get_connection() | ||||
|     """ | ||||
|     List all newsletters (newest first). | ||||
|     """ | ||||
|     conn   = get_connection() | ||||
|     cursor = conn.cursor() | ||||
|     cursor.execute("SELECT id, subject, body, sent_at FROM newsletters ORDER BY sent_at DESC") | ||||
|     results = cursor.fetchall() | ||||
|     newsletters = [ | ||||
|         {"id": rec[0], "subject": rec[1], "body": rec[2], "sent_at": rec[3]}  | ||||
|         for rec in results | ||||
|     ] | ||||
|     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 | ||||
|     ] | ||||
|     return render_template("newsletters.html", newsletters=newsletters) | ||||
| 
 | ||||
| @app.route("/newsletter/<int:newsletter_id>") | ||||
| 
 | ||||
| @app.route("/newsletter/<int:newsletter_id>", methods=["GET"]) | ||||
| def newsletter_detail(newsletter_id): | ||||
|     conn = get_connection() | ||||
|     """ | ||||
|     Show a single newsletter by its ID. | ||||
|     """ | ||||
|     conn   = get_connection() | ||||
|     cursor = conn.cursor() | ||||
|     cursor.execute("SELECT id, subject, body, sent_at FROM newsletters WHERE id = %s", (newsletter_id,)) | ||||
|     record = cursor.fetchone() | ||||
|     cursor.execute( | ||||
|         "SELECT id, subject, body, sent_at " | ||||
|       + "FROM newsletters WHERE id = %s", | ||||
|         (newsletter_id,) | ||||
|     ) | ||||
|     row = cursor.fetchone() | ||||
|     cursor.close() | ||||
|     conn.close() | ||||
| 
 | ||||
|     if record is None: | ||||
|     if not row: | ||||
|         return "Newsletter not found.", 404 | ||||
| 
 | ||||
|     newsletter = { | ||||
|         "id": record[0], | ||||
|         "subject": record[1], | ||||
|         "body": record[2], | ||||
|         "sent_at": record[3] | ||||
|         "id":      row[0], | ||||
|         "subject": row[1], | ||||
|         "body":    row[2], | ||||
|         "sent_at": row[3] | ||||
|     } | ||||
|     return render_template("newsletter_detail.html", newsletter=newsletter) | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
|     app.run(debug=True) | ||||
|     app.run(host="0.0.0.0", debug=True) | ||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue
	
	 Cipher Vance
						Cipher Vance