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 | 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 os | ||||||
|  | import time | ||||||
|  | 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 | ||||||
| from database import get_connection, init_db, add_email, remove_email |  | ||||||
| from dotenv import load_dotenv | from dotenv import load_dotenv | ||||||
| from collections import namedtuple | from database import init_db, get_connection, add_email, remove_email | ||||||
| 
 | 
 | ||||||
| load_dotenv() | 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__) | app = Flask(__name__) | ||||||
| init_db() | init_db() | ||||||
| 
 | 
 | ||||||
| def send_confirmation_email(email): | @app.before_request | ||||||
|     SMTP_SERVER = os.getenv('SMTP_SERVER') | def start_timer(): | ||||||
|     SMTP_PORT = int(os.getenv('SMTP_PORT', 465)) |     request._start_time = time.time() | ||||||
|     SMTP_USER = os.getenv('SMTP_USER') |  | ||||||
|     SMTP_PASSWORD = os.getenv('SMTP_PASSWORD') |  | ||||||
| 
 | 
 | ||||||
|     unsubscribe_link = f"{request.url_root}unsubscribe?email={email}" | @app.after_request | ||||||
| 
 | def log_request(response): | ||||||
|     subject = 'Thanks for subscribing!' |     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( |     html_body = render_template( | ||||||
|         'confirmation_email.html', |         "confirmation_email.html", | ||||||
|         unsubscribe_link=unsubscribe_link |         unsubscribe_link=unsubscribe_link | ||||||
|     ) |     ) | ||||||
| 
 | 
 | ||||||
|     msg = MIMEText(html_body, 'html', 'utf-8')  # Specify HTML |     msg = MIMEText(html_body, "html", "utf-8") | ||||||
|     msg['Subject'] = subject |     msg["Subject"] = subject | ||||||
|     msg['From'] = SMTP_USER |     msg["From"]    = SMTP_USER | ||||||
|     msg['To'] = email |     msg["To"]      = to_address | ||||||
| 
 | 
 | ||||||
|     try: |     try: | ||||||
|         server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10) |         with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, timeout=10) as server: | ||||||
|         server.login(SMTP_USER, SMTP_PASSWORD) |             server.login(SMTP_USER, SMTP_PASSWORD) | ||||||
|         server.sendmail(SMTP_USER, email, msg.as_string()) |             server.sendmail(SMTP_USER, [to_address], msg.as_string()) | ||||||
|         server.quit() |  | ||||||
|     except Exception as e: |     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(): | def index(): | ||||||
|     return render_template("index.html") |     return render_template("index.html") | ||||||
| 
 | 
 | ||||||
| @app.route("/subscribe", methods=["POST"]) | @app.route("/subscribe", methods=["POST"]) | ||||||
| def subscribe(): | def subscribe(): | ||||||
|     data = request.get_json() |     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 | ||||||
| 
 | 
 | ||||||
|     if add_email(email): |     if add_email(email): | ||||||
|         send_confirmation_email(email) |         unsubscribe_link = f"{request.url_root}unsubscribe?email={email}" | ||||||
|         return jsonify({"message": "Email has been added"}), 201 | 
 | ||||||
|     else: |         Thread( | ||||||
|         return jsonify({"error": "Email already exists"}), 400 |             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"]) | @app.route("/unsubscribe", methods=["GET"]) | ||||||
| def unsubscribe(): | def unsubscribe(): | ||||||
|  | @ -63,42 +88,57 @@ def unsubscribe(): | ||||||
| 
 | 
 | ||||||
|     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 |  | ||||||
| 
 | 
 | ||||||
| @app.route("/newsletters") | 
 | ||||||
|  | @app.route("/newsletters", methods=["GET"]) | ||||||
| def newsletters(): | def newsletters(): | ||||||
|     conn = get_connection() |     """ | ||||||
|  |     List all newsletters (newest first). | ||||||
|  |     """ | ||||||
|  |     conn   = get_connection() | ||||||
|     cursor = conn.cursor() |     cursor = conn.cursor() | ||||||
|     cursor.execute("SELECT id, subject, body, sent_at FROM newsletters ORDER BY sent_at DESC") |     cursor.execute( | ||||||
|     results = cursor.fetchall() |         "SELECT id, subject, body, sent_at " | ||||||
|     newsletters = [ |       + "FROM newsletters ORDER BY sent_at DESC" | ||||||
|         {"id": rec[0], "subject": rec[1], "body": rec[2], "sent_at": rec[3]}  |     ) | ||||||
|         for rec in results |     rows = cursor.fetchall() | ||||||
|     ] |  | ||||||
|     cursor.close() |     cursor.close() | ||||||
|     conn.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) |     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): | def newsletter_detail(newsletter_id): | ||||||
|     conn = get_connection() |     """ | ||||||
|  |     Show a single newsletter by its ID. | ||||||
|  |     """ | ||||||
|  |     conn   = get_connection() | ||||||
|     cursor = conn.cursor() |     cursor = conn.cursor() | ||||||
|     cursor.execute("SELECT id, subject, body, sent_at FROM newsletters WHERE id = %s", (newsletter_id,)) |     cursor.execute( | ||||||
|     record = cursor.fetchone() |         "SELECT id, subject, body, sent_at " | ||||||
|  |       + "FROM newsletters WHERE id = %s", | ||||||
|  |         (newsletter_id,) | ||||||
|  |     ) | ||||||
|  |     row = cursor.fetchone() | ||||||
|     cursor.close() |     cursor.close() | ||||||
|     conn.close() |     conn.close() | ||||||
| 
 | 
 | ||||||
|     if record is None: |     if not row: | ||||||
|         return "Newsletter not found.", 404 |         return "Newsletter not found.", 404 | ||||||
| 
 | 
 | ||||||
|     newsletter = { |     newsletter = { | ||||||
|         "id": record[0], |         "id":      row[0], | ||||||
|         "subject": record[1], |         "subject": row[1], | ||||||
|         "body": record[2], |         "body":    row[2], | ||||||
|         "sent_at": record[3] |         "sent_at": row[3] | ||||||
|     } |     } | ||||||
|     return render_template("newsletter_detail.html", newsletter=newsletter) |     return render_template("newsletter_detail.html", newsletter=newsletter) | ||||||
| 
 | 
 | ||||||
| if __name__ == "__main__": | 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