1212from pypsa_app .backend .database import SessionLocal
1313from pypsa_app .backend .models import Run , RunStatus
1414from pypsa_app .backend .services .backend_registry import backend_registry
15+ from pypsa_app .backend .services .callback import fire_callback_async
1516from pypsa_app .backend .tasks import import_run_outputs_task
1617
1718logger = logging .getLogger (__name__ )
1819
20+ # Hold references to fire-and-forget callback tasks to prevent garbage collection.
21+ _background_tasks : set [asyncio .Task ] = set ()
22+
1923# Statuses where the remote executor is done, no need to sync from Snakedispatch
2024SYNCED_STATUSES = {
2125 RunStatus .UPLOADING ,
4145]
4246
4347
44- def sync_run_from_job (run : Run , job : dict , db : Session ) -> None :
45- """Update a Run record from a Snakedispatch response dict."""
48+ _CALLBACK_STATUSES = SYNCED_STATUSES - {RunStatus .UPLOADING }
49+
50+
51+ def sync_run_from_job (run : Run , job : dict , db : Session ) -> bool :
52+ """Update a Run record from a Snakedispatch response dict.
53+
54+ Returns:
55+ True if a callback should be fired after the transaction commits.
56+ """
57+ old_status = run .status
4658 changed = False
4759 for field in _SYNC_FIELDS :
4860 new_val = job .get (field )
@@ -65,7 +77,7 @@ def sync_run_from_job(run: Run, job: dict, db: Session) -> None:
6577 run .status = RunStatus .UPLOADING
6678 db .flush ()
6779 import_run_outputs_task .apply_async (args = (str (run .job_id ),))
68- return
80+ return False
6981 if completed_with_import_pending :
7082 run .status = RunStatus .COMPLETED
7183 changed = True
@@ -76,38 +88,65 @@ def sync_run_from_job(run: Run, job: dict, db: Session) -> None:
7688 if changed :
7789 db .flush ()
7890
91+ return run .status in _CALLBACK_STATUSES and old_status not in _CALLBACK_STATUSES
92+
93+
94+ def sync_non_terminal_runs () -> list [dict ]:
95+ """Poll all backends and update runs that haven't reached a terminal state.
7996
80- def sync_non_terminal_runs () -> None :
81- """Poll all backends and update runs that haven't reached a terminal state."""
97+ Returns:
98+ List of callback dicts ``{"url": ..., "payload": ...}`` to be fired
99+ by the async caller after the DB session is closed.
100+ """
101+ callbacks : list [dict ] = []
82102 db = SessionLocal ()
83103 try :
84104 non_terminal = db .query (Run ).filter (Run .status .notin_ (SYNCED_STATUSES )).all ()
85105 if not non_terminal :
86- return
106+ return callbacks
87107
88108 for backend_id , client in backend_registry .all_clients ().items ():
89109 backend_runs = [r for r in non_terminal if r .backend_id == backend_id ]
90110 if not backend_runs :
91111 continue
92112 try :
93113 jobs_by_id = {j ["job_id" ]: j for j in client .list_jobs ()}
114+ callback_runs : list [Run ] = []
94115 for run in backend_runs :
95116 job = jobs_by_id .get (str (run .job_id ))
96- if job :
97- sync_run_from_job (run , job , db )
117+ if job and sync_run_from_job ( run , job , db ) :
118+ callback_runs . append (run )
98119 db .commit ()
120+ callbacks .extend (
121+ {
122+ "url" : str (run .callback_url ),
123+ "payload" : {
124+ "run_id" : str (run .job_id ),
125+ "status" : run .status .value ,
126+ },
127+ }
128+ for run in callback_runs
129+ if run .callback_url
130+ )
99131 except Exception :
100132 db .rollback ()
101133 logger .warning ("Sync failed for backend %s" , backend_id , exc_info = True )
102134 finally :
103135 db .close ()
136+ return callbacks
104137
105138
106139async def run_sync_loop (interval : float = 10.0 ) -> None :
107140 """Periodically sync non-terminal runs in a background thread."""
108141 while True :
109142 await asyncio .sleep (interval )
110143 try :
111- await asyncio .to_thread (sync_non_terminal_runs )
144+ callbacks = await asyncio .to_thread (sync_non_terminal_runs )
145+ for cb in callbacks :
146+ task = asyncio .create_task (
147+ fire_callback_async (cb ["url" ], cb ["payload" ])
148+ )
149+ _background_tasks .add (task )
150+ task .add_done_callback (_background_tasks .discard )
112151 except Exception :
113152 logger .warning ("Background run sync failed" , exc_info = True )
0 commit comments