11from fastapi_sqlalchemy import db
2- from sqlalchemy import desc
2+ from sqlalchemy import desc , create_engine
3+ from sqlalchemy .orm import sessionmaker
34from datetime import datetime
5+ import time
6+ import logging
7+ import threading
8+ from src .configuration .Settings import settings
49from generated_src .lleida_hack_mail_api_client .models .mail_create import MailCreate
510from collections import Counter
611
@@ -40,6 +45,65 @@ class EventService(BaseService):
4045 hacker_service : HackerService = None
4146 company_service : CompanyService = None
4247 mail_client : MailClient = None
48+ # background sending job tracking: event_id -> job info
49+ _sending_jobs = {}
50+ _sending_jobs_lock = threading .Lock ()
51+
52+ def _start_job (self , event_id : int , job_type : str , total : int ):
53+ with self ._sending_jobs_lock :
54+ self ._sending_jobs [event_id ] = {
55+ "type" : job_type ,
56+ "start" : datetime .now (),
57+ "finish" : None ,
58+ "total" : int (total ) if total is not None else 0 ,
59+ "sent" : 0 ,
60+ "status" : "running" ,
61+ }
62+
63+ def _increment_sent (self , event_id : int ):
64+ with self ._sending_jobs_lock :
65+ job = self ._sending_jobs .get (event_id )
66+ if job and job .get ("status" ) == "running" :
67+ job ["sent" ] = job .get ("sent" , 0 ) + 1
68+
69+ def _finish_job (self , event_id : int ):
70+ with self ._sending_jobs_lock :
71+ job = self ._sending_jobs .get (event_id )
72+ if job :
73+ job ["status" ] = "finished"
74+ job ["finish" ] = datetime .now ()
75+
76+ def is_sending (self , event_id : int ) -> bool :
77+ with self ._sending_jobs_lock :
78+ job = self ._sending_jobs .get (event_id )
79+ return bool (job and job .get ("status" ) == "running" )
80+
81+ def get_send_progress (self , event_id : int ):
82+ with self ._sending_jobs_lock :
83+ job = self ._sending_jobs .get (event_id )
84+ if not job :
85+ return {"running" : False }
86+ # copy minimal fields to compute without lock
87+ start = job .get ("start" )
88+ sent = job .get ("sent" , 0 )
89+ total = job .get ("total" , 0 )
90+ status = job .get ("status" )
91+ finish = job .get ("finish" )
92+ elapsed = (datetime .now () - start ).total_seconds () if start else 0
93+ estimated_remaining = None
94+ if sent > 0 and total > sent :
95+ avg = elapsed / sent
96+ estimated_remaining = int (max (0 , avg * (total - sent )))
97+
98+ return {
99+ "running" : status == "running" ,
100+ "sent" : int (sent ),
101+ "total" : int (total ),
102+ "elapsed_seconds" : int (elapsed ),
103+ "estimated_remaining_seconds" : estimated_remaining ,
104+ "started_at" : start .isoformat () if start else None ,
105+ "finished_at" : finish .isoformat () if finish else None ,
106+ }
43107
44108 def get_all (self ):
45109 return db .session .query (Event ).filter (Event .archived .is_ (False )).all ()
@@ -996,10 +1060,66 @@ def send_slack_mail(self, event_id: int, slackUrl: str, data: BaseToken):
9961060 )
9971061 )
9981062 # send the created mail
999- self .mail_client .send_mail_by_id (mail .id )
1063+ resp = self .mail_client .send_mail_by_id (mail .id )
10001064
10011065 db .session .commit ()
10021066
1067+ @BaseService .needs_service (MailClient )
1068+ def send_slack_mail_background (self , event_id : int , slackUrl : str , delay : float = 0.2 ):
1069+ """
1070+ Background-safe sender: creates its own DB session and sends slack invite mails
1071+ to accepted hackers with an optional `delay` between sends to avoid throttling.
1072+ """
1073+ engine = create_engine (settings .database .url )
1074+ SessionLocal = sessionmaker (autocommit = False , autoflush = False , bind = engine )
1075+ session = SessionLocal ()
1076+ try :
1077+ event = session .query (Event ).filter (Event .id == event_id ).first ()
1078+ if event is None or event .archived :
1079+ return
1080+
1081+ hackers = event .accepted_hackers
1082+ total = len (hackers ) if hackers is not None else 0
1083+ # start tracking job
1084+ try :
1085+ self ._start_job (event_id , "slack" , total )
1086+ except Exception :
1087+ # best-effort tracking, don't fail send if tracking setup fails
1088+ pass
1089+
1090+ for hacker in hackers :
1091+ try :
1092+ mail = self .mail_client .create_mail (
1093+ MailCreate (
1094+ template_id = self .mail_client .get_internall_template_id (
1095+ InternalTemplate .EVENT_SLACK_INVITE
1096+ ),
1097+ subject = "HackEPS2025 slack invitation" ,
1098+ receiver_id = str (hacker .id ),
1099+ receiver_mail = str (hacker .email ),
1100+ fields = slackUrl ,
1101+ )
1102+ )
1103+ resp = self .mail_client .send_mail_by_id (mail .id )
1104+ # increment progress
1105+ try :
1106+ self ._increment_sent (event_id )
1107+ except Exception :
1108+ pass
1109+ session .commit ()
1110+ if delay and delay > 0 :
1111+ time .sleep (delay )
1112+ except Exception :
1113+ session .rollback ()
1114+ continue
1115+ # finish tracking
1116+ try :
1117+ self ._finish_job (event_id )
1118+ except Exception :
1119+ pass
1120+ finally :
1121+ session .close ()
1122+
10031123 @BaseService .needs_service (MailClient )
10041124 def send_reminder_mails (
10051125 self ,
@@ -1041,7 +1161,8 @@ def send_reminder_mails(
10411161 except Exception :
10421162 days_left = 0
10431163
1044- fields = f"{ hacker .name } ,{ event .name } ,{ days_left } ,{ reg .confirm_assistance_token } "
1164+ # fields order: name, days_left, token, event_name
1165+ fields = f"{ hacker .name } ,{ days_left } ,{ reg .confirm_assistance_token } ,{ event .name } "
10451166
10461167 mail = self .mail_client .create_mail (
10471168 MailCreate (
@@ -1055,10 +1176,90 @@ def send_reminder_mails(
10551176 )
10561177 )
10571178 # send the created mail
1058- self .mail_client .send_mail_by_id (mail .id )
1179+ resp = self .mail_client .send_mail_by_id (mail .id )
10591180 db .session .commit ()
10601181 except Exception as e :
10611182 db .session .rollback ()
10621183 # Optionally log the error here, e.g.:
10631184 # print(f"Failed to send reminder to hacker {hacker.id}: {e}")
1064- continue
1185+ continue
1186+
1187+ @BaseService .needs_service (MailClient )
1188+ def send_reminder_mails_background (self , event_id : int , delay : float = 0.0 ):
1189+ """
1190+ Background-safe sender: creates its own DB session and sends reminder mails
1191+ to accepted hackers. Optional `delay` between sends to avoid throttling.
1192+ """
1193+ engine = create_engine (settings .database .url )
1194+ SessionLocal = sessionmaker (autocommit = False , autoflush = False , bind = engine )
1195+ session = SessionLocal ()
1196+ try :
1197+ event = session .query (Event ).filter (Event .id == event_id ).first ()
1198+ if event is None or event .archived :
1199+ return
1200+
1201+ # build list of eligible (hacker, reg) to have accurate total
1202+ hackers = event .accepted_hackers
1203+ eligible = []
1204+ for hacker in hackers :
1205+ reg = (
1206+ session .query (HackerRegistration )
1207+ .filter (
1208+ HackerRegistration .user_id == hacker .id ,
1209+ HackerRegistration .event_id == event .id ,
1210+ )
1211+ .first ()
1212+ )
1213+ if reg is None or reg .confirmed_assistance :
1214+ continue
1215+ eligible .append ((hacker , reg ))
1216+
1217+ total = len (eligible )
1218+ try :
1219+ self ._start_job (event_id , "reminder" , total )
1220+ except Exception :
1221+ pass
1222+
1223+ for hacker , reg in eligible :
1224+ try :
1225+ # ensure there is a confirmation token for this registration
1226+ if not reg .confirm_assistance_token :
1227+ reg .confirm_assistance_token = AssistenceToken (hacker , event .id ).to_token ()
1228+
1229+ try :
1230+ delta = event .start_date - datetime .now ()
1231+ days_left = max (0 , int (delta .total_seconds () // 86400 ))
1232+ except Exception :
1233+ days_left = 0
1234+
1235+ fields = f"{ hacker .name } ,{ days_left } ,{ reg .confirm_assistance_token } ,{ event .name } "
1236+
1237+ mail = self .mail_client .create_mail (
1238+ MailCreate (
1239+ template_id = self .mail_client .get_internall_template_id (
1240+ InternalTemplate .EVENT_HACKER_REMINDER
1241+ ),
1242+ subject = f"{ event .name } - Recordatori de confirmació d'assistència" ,
1243+ receiver_id = str (hacker .id ),
1244+ receiver_mail = str (hacker .email ),
1245+ fields = fields ,
1246+ )
1247+ )
1248+
1249+ resp = self .mail_client .send_mail_by_id (mail .id )
1250+ try :
1251+ self ._increment_sent (event_id )
1252+ except Exception :
1253+ pass
1254+ session .commit ()
1255+ if delay and delay > 0 :
1256+ time .sleep (delay )
1257+ except Exception :
1258+ session .rollback ()
1259+ continue
1260+ try :
1261+ self ._finish_job (event_id )
1262+ except Exception :
1263+ pass
1264+ finally :
1265+ session .close ()
0 commit comments