77from app .core .config import settings
88from app .database .db import db_manager
99from app .schemas .db_schemas import JobStatus
10- from app .schemas .kubeflow_schemas import KubeflowStatusEnum , TrainingJobStatus
10+ from app .schemas .kubeflow_schemas import (
11+ KubeflowStatusEnum ,
12+ TrainingJobStatus ,
13+ DatabaseStatusEnum ,
14+ )
1115from app .utils .kf_config import kubeflow_api
1216from app .utils .S3Handler import s3_handler
1317from app .utils .kueue_helpers import get_kueue_queue , get_kubeflow_queue
18+ from app .utils .slack_helpers import send_slack_notification
1419
1520logger = logging .getLogger (__name__ )
1621
@@ -19,6 +24,8 @@ class JobMonitor:
1924 def __init__ (self ):
2025 self .stop_monitoring = False
2126 self .monitoring_task = None
27+ self .active_jobs = set ()
28+ self .notified_queued_jobs = set ()
2229
2330 async def _get_queue_info (self ) -> dict [str , int ]:
2431 """Get queue information with fallback"""
@@ -128,13 +135,33 @@ async def monitor_jobs(self):
128135 while not self .stop_monitoring :
129136 try :
130137 # Get current state from Kubeflow API
131- jobs = kubeflow_api .list_jobs (namespace = settings .NAMESPACE )
132- queue_positions = await self ._get_queue_info ()
138+ api_jobs = {
139+ job .metadata .name : job
140+ for job in kubeflow_api .list_jobs (namespace = settings .NAMESPACE )
141+ }
142+ current_job_ids = set (api_jobs .keys ())
143+
144+ # Find jobs that have disappeared
145+ disappeared_jobs = self .active_jobs - current_job_ids
146+ for job_id in disappeared_jobs :
147+ job_info = await db_manager .get_job (job_id )
148+ if (
149+ job_info
150+ and job_info .status not in TrainingJobStatus .running_states
151+ ):
152+ logger .warning (
153+ f"Job { job_id } disappeared from API, probably canceled by user."
154+ )
155+ await send_slack_notification (
156+ f"Job `{ job_info .status } `: `{ job_info .job_name } ({ job_id } )` by `{ job_info .user_id } `\n Model: `{ job_info .model_name } ` | Started: `{ job_info .created_at } `"
157+ )
158+ self .notified_queued_jobs .discard (job_id )
133159
134- for job in jobs :
135- job_id = job . metadata . name
160+ self . active_jobs = current_job_ids
161+ queue_positions = await self . _get_queue_info ()
136162
137- if not job .status .conditions :
163+ for job_id , job in api_jobs .items ():
164+ if not job .status or not job .status .conditions :
138165 logger .warning (f"Job conditions not ready for { job_id } " )
139166 await asyncio .sleep (0.1 )
140167 continue
@@ -160,6 +187,31 @@ async def monitor_jobs(self):
160187 logger .info (
161188 f"Job { job_id } status changed from { prev_job_info .status } to { status } "
162189 )
190+ # Notify if job in queue
191+ if (
192+ TrainingJobStatus .map_status (status )
193+ == DatabaseStatusEnum .queued
194+ and job_id not in self .notified_queued_jobs
195+ ):
196+ await send_slack_notification (
197+ f"Job `{ TrainingJobStatus .map_status (status )} `: `{ prev_job_info .job_name } ({ job_id } )` by `{ prev_job_info .user_id } `\n Model: `{ prev_job_info .model_name } ` | Queue Position: `{ queue_positions .get (job_id , 'N/A' )} `"
198+ )
199+ self .notified_queued_jobs .add (job_id )
200+ # Notify on start
201+ elif (
202+ status == KubeflowStatusEnum .running
203+ and prev_job_info .status .lower ()
204+ != KubeflowStatusEnum .running .lower ()
205+ ):
206+ await send_slack_notification (
207+ f"Job `{ TrainingJobStatus .map_status (status )} `: `{ prev_job_info .job_name } ({ job_id } )` by `{ prev_job_info .user_id } `\n Model: `{ prev_job_info .model_name } ` | Started: `{ prev_job_info .created_at } `"
208+ )
209+ # Notify on stop
210+ elif status in TrainingJobStatus .stopped_states :
211+ await send_slack_notification (
212+ f"Job `{ TrainingJobStatus .map_status (status )} `: `{ prev_job_info .job_name } ({ job_id } )` by `{ prev_job_info .user_id } `\n Model: `{ prev_job_info .model_name } ` | Started: `{ prev_job_info .created_at } ` | Ended: `{ job .status .completion_time } `"
213+ )
214+ self .notified_queued_jobs .discard (job_id )
163215
164216 # Update status in database
165217 job_info = await self ._update_job_status (
@@ -188,6 +240,7 @@ async def monitor_jobs(self):
188240 logger .error (
189241 f"Job { job_id } failed. Manual investigation required."
190242 )
243+ self .active_jobs .discard (job_id )
191244
192245 except Exception as e :
193246 logger .error (f"Error in job monitoring loop: { e } " , exc_info = True )
0 commit comments