From 25af7a836d13bc21d555698a0082608fd8223a1d Mon Sep 17 00:00:00 2001 From: NguyenND Date: Fri, 17 Apr 2026 14:24:16 -0400 Subject: [PATCH] 04/17 Update: sends email to each IT Staff from email ingestion ticket --- app/services/notification_service.py | 30 +++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/app/services/notification_service.py b/app/services/notification_service.py index 3660738..dd5d6ba 100644 --- a/app/services/notification_service.py +++ b/app/services/notification_service.py @@ -107,18 +107,34 @@ def create_notification(user_id, notif_type, title, message, ticket_id=None, lin def send_email(subject, recipients, html_body): """ - Send an email in a background thread so it never blocks the request. + Send an email in a background thread so it never blocks the caller. Flask-Mail opens an SMTP connection synchronously. Running it in the main request thread means a slow or failing SMTP server delays the entire response — including the WebSocket notification push that follows. - Using a daemon thread isolates SMTP failures from the request lifecycle. + Using a background thread isolates SMTP failures from the request lifecycle. + + Thread daemon mode + ------------------ + When called from a web request context, the thread is spawned as a daemon + (daemon=True) so it does not prevent the server process from exiting cleanly. + + When called from an APScheduler background job (no active request context), + the thread is spawned as a non-daemon (daemon=False). Daemon threads are + killed as soon as the parent thread exits — since the APScheduler job thread + finishes quickly, a daemon email thread is terminated before SMTP delivery + completes, causing silent email loss. A non-daemon thread survives until + the SMTP handshake finishes regardless of the job thread's lifetime. """ from threading import Thread - from flask import current_app + from flask import current_app, has_request_context app = current_app._get_current_object() # real app, not the proxy + # Determine daemon mode based on whether we are inside a live HTTP request. + # APScheduler jobs and other background callers have no request context. + is_daemon = has_request_context() + def _send(): with app.app_context(): try: @@ -128,7 +144,7 @@ def send_email(subject, recipients, html_body): except Exception as exc: logger.error(f'[EMAIL ERROR] Failed to send "{subject}" to {recipients}: {exc}') - t = Thread(target=_send, daemon=True) + t = Thread(target=_send, daemon=is_daemon) t.start() @@ -502,6 +518,10 @@ def send_satisfaction_survey(ticket): f'recipient={creator_email}: {exc}', exc_info=True ) - t = Thread(target=_send, daemon=True) + # Mirror the daemon-mode logic from send_email(): non-daemon when called + # outside a request context (e.g. from the SLA APScheduler job) so the + # thread is not killed before SMTP delivery completes. + from flask import has_request_context + t = Thread(target=_send, daemon=has_request_context()) t.start() logger.info(f'[SURVEY] Email thread started for ticket_id={ticket_id}') \ No newline at end of file