99from sqlalchemy import select
1010from sqlalchemy .ext .asyncio import AsyncSession
1111
12- from ..core .database import get_db , async_session_factory
12+ from ..core .database import get_db , sync_session_factory
1313from ..core .deps import get_current_user
1414from ..models .db_models import User , Project , Job
1515from ..models .schemas import (
@@ -60,10 +60,11 @@ async def run_analysis(
6060 p = storage .get_dataset_path (project_id , ytest_dataset_id )
6161 ytest_path = str (p ) if p else ""
6262
63- # Create job record in database
63+ # Create job record in database — commit immediately so the background
64+ # task (which uses a separate sync DB connection) can see the job.
6465 job = Job (project_id = project_id , status = "pending" , config = config .model_dump ())
6566 db .add (job )
66- await db .flush ()
67+ await db .commit ()
6768
6869 job_dir = Path (storage .settings .project_dir ) / project_id / "jobs" / job .id
6970 job_dir .mkdir (parents = True , exist_ok = True )
@@ -86,8 +87,11 @@ async def run_analysis(
8687
8788
8889def _run_job (job_id : str , project_id : str , param_path : str ) -> None :
89- """Execute gpredomics in a subprocess so we can capture Rust log output."""
90- import asyncio
90+ """Execute gpredomics in a subprocess so we can capture Rust log output.
91+
92+ Runs in a Starlette thread pool — uses synchronous DB sessions to avoid
93+ event-loop conflicts with the async engine.
94+ """
9195 import os
9296 import subprocess
9397 import sys
@@ -96,86 +100,75 @@ def _run_job(job_id: str, project_id: str, param_path: str) -> None:
96100 log_path = job_dir / "console.log"
97101 results_path = job_dir / "results.json"
98102
99- async def _execute ():
100- # Mark job as running
101- async with async_session_factory () as db :
102- result = await db .execute (select (Job ).where (Job .id == job_id ))
103- job = result .scalar_one ()
104- job .status = "running"
105- await db .commit ()
106-
107- try :
108- if not engine .HAS_ENGINE :
109- # Mock mode — no subprocess needed
110- mock_results = engine ._mock_results ()
111- storage .save_job_result (project_id , job_id , mock_results )
112- with open (log_path , "w" ) as lf :
113- lf .write ("[mock] Engine not available, using mock results\n " )
114-
115- async with async_session_factory () as db :
116- result = await db .execute (select (Job ).where (Job .id == job_id ))
117- job = result .scalar_one ()
118- job .status = "completed"
119- job .results_path = str (results_path )
120- job .completed_at = datetime .now (timezone .utc )
121- await db .commit ()
122- return
123-
124- # Run worker subprocess — stream output line-by-line for live console
125- worker_module = "app.services.worker"
126- env = os .environ .copy ()
127- env ["PYTHONUNBUFFERED" ] = "1"
128-
129- proc = subprocess .Popen (
130- [sys .executable , "-u" , "-m" , worker_module , param_path , str (results_path )],
131- stdout = subprocess .PIPE ,
132- stderr = subprocess .STDOUT ,
133- cwd = str (Path (__file__ ).resolve ().parents [2 ]), # backend/
134- env = env ,
135- )
136-
137- # Read output line-by-line and flush to log file immediately
138- with open (log_path , "w" ) as lf :
139- for line in proc .stdout :
140- lf .write (line .decode ("utf-8" , errors = "replace" ))
141- lf .flush ()
142-
143- proc .wait ()
103+ # Mark job as running
104+ with sync_session_factory () as db :
105+ job = db .execute (select (Job ).where (Job .id == job_id )).scalar_one ()
106+ job .status = "running"
107+ db .commit ()
144108
145- if proc .returncode != 0 :
146- error_msg = log_path .read_text ()[- 500 :] if log_path .exists () else "Unknown error"
147- raise RuntimeError (f"Worker exited with code { proc .returncode } : { error_msg } " )
109+ try :
110+ if not engine .HAS_ENGINE :
111+ # Mock mode — no subprocess needed
112+ mock_results = engine ._mock_results ()
113+ storage .save_job_result (project_id , job_id , mock_results )
114+ with open (log_path , "w" ) as lf :
115+ lf .write ("[mock] Engine not available, using mock results\n " )
148116
149- async with async_session_factory () as db :
150- result = await db .execute (select (Job ).where (Job .id == job_id ))
151- job = result .scalar_one ()
117+ with sync_session_factory () as db :
118+ job = db .execute (select (Job ).where (Job .id == job_id )).scalar_one ()
152119 job .status = "completed"
153120 job .results_path = str (results_path )
154121 job .completed_at = datetime .now (timezone .utc )
155- await db .commit ()
122+ db .commit ()
123+ return
124+
125+ # Run worker subprocess — stream output line-by-line for live console
126+ worker_module = "app.services.worker"
127+ env = os .environ .copy ()
128+ env ["PYTHONUNBUFFERED" ] = "1"
129+
130+ proc = subprocess .Popen (
131+ [sys .executable , "-u" , "-m" , worker_module , param_path , str (results_path )],
132+ stdout = subprocess .PIPE ,
133+ stderr = subprocess .STDOUT ,
134+ cwd = str (Path (__file__ ).resolve ().parents [2 ]), # backend/
135+ env = env ,
136+ )
156137
157- logger .info ("Job %s completed" , job_id )
138+ # Read output line-by-line and flush to log file immediately
139+ with open (log_path , "w" ) as lf :
140+ for line in proc .stdout :
141+ lf .write (line .decode ("utf-8" , errors = "replace" ))
142+ lf .flush ()
158143
159- except Exception as e :
160- # Append error to log
161- with open (log_path , "a" ) as lf :
162- lf .write (f"\n [error] { e } \n " )
144+ proc .wait ()
163145
164- async with async_session_factory () as db :
165- result = await db .execute (select (Job ).where (Job .id == job_id ))
166- job = result .scalar_one ()
167- job .status = "failed"
168- job .error_message = str (e )
169- job .completed_at = datetime .now (timezone .utc )
170- await db .commit ()
146+ if proc .returncode != 0 :
147+ error_msg = log_path .read_text ()[- 500 :] if log_path .exists () else "Unknown error"
148+ raise RuntimeError (f"Worker exited with code { proc .returncode } : { error_msg } " )
171149
172- logger .error ("Job %s failed: %s" , job_id , e )
150+ with sync_session_factory () as db :
151+ job = db .execute (select (Job ).where (Job .id == job_id )).scalar_one ()
152+ job .status = "completed"
153+ job .results_path = str (results_path )
154+ job .completed_at = datetime .now (timezone .utc )
155+ db .commit ()
173156
174- loop = asyncio .new_event_loop ()
175- try :
176- loop .run_until_complete (_execute ())
177- finally :
178- loop .close ()
157+ logger .info ("Job %s completed" , job_id )
158+
159+ except Exception as e :
160+ # Append error to log
161+ with open (log_path , "a" ) as lf :
162+ lf .write (f"\n [error] { e } \n " )
163+
164+ with sync_session_factory () as db :
165+ job = db .execute (select (Job ).where (Job .id == job_id )).scalar_one ()
166+ job .status = "failed"
167+ job .error_message = str (e )
168+ job .completed_at = datetime .now (timezone .utc )
169+ db .commit ()
170+
171+ logger .error ("Job %s failed: %s" , job_id , e )
179172
180173
181174@router .get ("/{project_id}/jobs/{job_id}/logs" )
0 commit comments