task_manager.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. """Task management service for Web-Local system dispatch (P4-M2).
  2. Handles task creation, dispatch, status tracking, progress updates,
  3. and result reception from local simulation executor.
  4. """
  5. import json
  6. import os
  7. import uuid
  8. from datetime import datetime
  9. from typing import Dict, List, Optional, Any
  10. from pathlib import Path
  11. from ..database import SessionLocal
  12. from ..models.task import Task
  13. class TaskManager:
  14. """Manages simulation tasks between Web and Local systems."""
  15. TASK_STATUSES = [
  16. "pending", # Created, waiting for dispatch
  17. "dispatched", # Sent to local executor
  18. "running", # Local executor is running
  19. "completed", # All points completed successfully
  20. "failed", # Task failed
  21. "cancelled", # User cancelled
  22. ]
  23. def __init__(self, output_dir: Optional[str] = None):
  24. self.output_dir = output_dir or os.path.join(
  25. os.path.dirname(os.path.dirname(os.path.dirname(__file__))),
  26. "output", "tasks"
  27. )
  28. os.makedirs(self.output_dir, exist_ok=True)
  29. def create_task(
  30. self,
  31. plan_id: Optional[int],
  32. plan_data: Dict[str, Any],
  33. parameters: List[Dict[str, Any]],
  34. task_name: Optional[str] = None,
  35. priority: int = 5,
  36. created_by: str = "web",
  37. ) -> Dict[str, Any]:
  38. """Create a new simulation task.
  39. Args:
  40. plan_id: Associated plan ID (optional)
  41. plan_data: Full plan data (boundary conditions, topology, etc.)
  42. parameters: List of parameter sets to simulate
  43. task_name: Optional task name
  44. priority: Task priority (1-10, higher = more urgent)
  45. created_by: Creator identifier
  46. Returns:
  47. Created task dict
  48. """
  49. task_uuid = str(uuid.uuid4())[:8]
  50. task_name = task_name or f"task_{task_uuid}"
  51. task_dir = os.path.join(self.output_dir, f"{datetime.now().strftime('%Y%m%d_%H%M%S')}_{task_name}")
  52. os.makedirs(task_dir, exist_ok=True)
  53. # Write task file for local executor
  54. task_file = os.path.join(task_dir, "task.json")
  55. task_payload = {
  56. "task_id": task_uuid,
  57. "task_name": task_name,
  58. "plan_id": plan_id,
  59. "plan_data": plan_data,
  60. "parameters": parameters,
  61. "priority": priority,
  62. "created_at": datetime.now().isoformat(),
  63. "total_points": len(parameters),
  64. }
  65. with open(task_file, "w", encoding="utf-8") as f:
  66. json.dump(task_payload, f, ensure_ascii=False, indent=2)
  67. # Save to database
  68. with SessionLocal() as db:
  69. db_task = Task(
  70. task_id=task_uuid,
  71. task_name=task_name,
  72. plan_id=plan_id,
  73. status="pending",
  74. priority=priority,
  75. total_points=len(parameters),
  76. completed_points=0,
  77. task_dir=task_dir,
  78. task_file=task_file,
  79. created_by=created_by,
  80. created_at=datetime.now(),
  81. )
  82. db.add(db_task)
  83. db.commit()
  84. db.refresh(db_task)
  85. return self._task_to_dict(db_task)
  86. def dispatch_task(self, task_id: str) -> Dict[str, Any]:
  87. """Mark task as dispatched and ready for local executor.
  88. Args:
  89. task_id: Task UUID
  90. Returns:
  91. Updated task dict
  92. """
  93. with SessionLocal() as db:
  94. task = db.query(Task).filter(Task.task_id == task_id).first()
  95. if not task:
  96. raise ValueError(f"Task {task_id} not found")
  97. if task.status != "pending":
  98. raise ValueError(f"Task {task_id} is not pending (status: {task.status})")
  99. task.status = "dispatched"
  100. task.dispatched_at = datetime.now()
  101. db.commit()
  102. db.refresh(task)
  103. return self._task_to_dict(task)
  104. def update_progress(
  105. self,
  106. task_id: str,
  107. current_point: int,
  108. total_points: Optional[int] = None,
  109. current_params: Optional[Dict[str, Any]] = None,
  110. elapsed_time: Optional[float] = None,
  111. ) -> Dict[str, Any]:
  112. """Update task progress from local executor.
  113. Args:
  114. task_id: Task UUID
  115. current_point: Current point index (0-based)
  116. total_points: Total points (optional, will use stored value)
  117. current_params: Current parameter values being simulated
  118. elapsed_time: Elapsed time in seconds
  119. Returns:
  120. Updated task dict
  121. """
  122. with SessionLocal() as db:
  123. task = db.query(Task).filter(Task.task_id == task_id).first()
  124. if not task:
  125. raise ValueError(f"Task {task_id} not found")
  126. if task.status in ("dispatched", "running"):
  127. task.status = "running"
  128. task.started_at = task.started_at or datetime.now()
  129. task.completed_points = current_point
  130. if total_points:
  131. task.total_points = total_points
  132. # Update progress metadata
  133. progress_data = {
  134. "current_point": current_point,
  135. "current_params": current_params,
  136. "elapsed_time": elapsed_time,
  137. "updated_at": datetime.now().isoformat(),
  138. }
  139. existing_progress = json.loads(task.progress_data or "{}")
  140. existing_progress.update(progress_data)
  141. task.progress_data = json.dumps(existing_progress, ensure_ascii=False)
  142. db.commit()
  143. db.refresh(task)
  144. return self._task_to_dict(task)
  145. def report_results(
  146. self,
  147. task_id: str,
  148. results: List[Dict[str, Any]],
  149. metrics: Optional[Dict[str, Any]] = None,
  150. logs: Optional[str] = None,
  151. duration: Optional[float] = None,
  152. status: str = "completed",
  153. ) -> Dict[str, Any]:
  154. """Report final results from local executor.
  155. Args:
  156. task_id: Task UUID
  157. results: List of simulation result dicts
  158. metrics: Aggregated metrics
  159. logs: Program logs
  160. duration: Total duration in seconds
  161. status: Final status (completed/failed)
  162. Returns:
  163. Updated task dict
  164. """
  165. with SessionLocal() as db:
  166. task = db.query(Task).filter(Task.task_id == task_id).first()
  167. if not task:
  168. raise ValueError(f"Task {task_id} not found")
  169. task.status = status
  170. task.completed_at = datetime.now()
  171. task.completed_points = len(results)
  172. if duration:
  173. task.duration = duration
  174. # Save results to file
  175. results_file = os.path.join(task.task_dir, "results.json")
  176. with open(results_file, "w", encoding="utf-8") as f:
  177. json.dump({"results": results, "metrics": metrics, "logs": logs}, f, ensure_ascii=False, indent=2)
  178. task.results_file = results_file
  179. if metrics:
  180. task.result_metrics = json.dumps(metrics, ensure_ascii=False)
  181. db.commit()
  182. db.refresh(task)
  183. return self._task_to_dict(task)
  184. def get_task(self, task_id: str) -> Optional[Dict[str, Any]]:
  185. """Get task by ID."""
  186. with SessionLocal() as db:
  187. task = db.query(Task).filter(Task.task_id == task_id).first()
  188. if not task:
  189. return None
  190. return self._task_to_dict(task)
  191. def list_tasks(
  192. self,
  193. status: Optional[str] = None,
  194. plan_id: Optional[int] = None,
  195. limit: int = 50,
  196. offset: int = 0,
  197. ) -> Dict[str, Any]:
  198. """List tasks with filters."""
  199. with SessionLocal() as db:
  200. query = db.query(Task)
  201. if status:
  202. query = query.filter(Task.status == status)
  203. if plan_id:
  204. query = query.filter(Task.plan_id == plan_id)
  205. total = query.count()
  206. tasks = query.order_by(Task.created_at.desc()).offset(offset).limit(limit).all()
  207. return {
  208. "total": total,
  209. "limit": limit,
  210. "offset": offset,
  211. "tasks": [self._task_to_dict(t) for t in tasks],
  212. }
  213. def cancel_task(self, task_id: str) -> Dict[str, Any]:
  214. """Cancel a pending or running task."""
  215. with SessionLocal() as db:
  216. task = db.query(Task).filter(Task.task_id == task_id).first()
  217. if not task:
  218. raise ValueError(f"Task {task_id} not found")
  219. if task.status in ("completed", "failed", "cancelled"):
  220. raise ValueError(f"Task {task_id} already finished (status: {task.status})")
  221. task.status = "cancelled"
  222. task.completed_at = datetime.now()
  223. db.commit()
  224. db.refresh(task)
  225. return self._task_to_dict(task)
  226. def get_task_results(self, task_id: str) -> Optional[Dict[str, Any]]:
  227. """Get task results file content."""
  228. task = self.get_task(task_id)
  229. if not task or not task.get("results_file"):
  230. return None
  231. if not os.path.exists(task["results_file"]):
  232. return None
  233. with open(task["results_file"], "r", encoding="utf-8") as f:
  234. return json.load(f)
  235. def _task_to_dict(self, task: Task) -> Dict[str, Any]:
  236. """Convert Task ORM object to dict."""
  237. result = {
  238. "id": task.id,
  239. "task_id": task.task_id,
  240. "task_name": task.task_name,
  241. "plan_id": task.plan_id,
  242. "status": task.status,
  243. "priority": task.priority,
  244. "total_points": task.total_points,
  245. "completed_points": task.completed_points,
  246. "progress": round((task.completed_points / task.total_points * 100), 1) if task.total_points else 0,
  247. "task_dir": task.task_dir,
  248. "task_file": task.task_file,
  249. "results_file": task.results_file,
  250. "created_by": task.created_by,
  251. "created_at": task.created_at.isoformat() if task.created_at else None,
  252. "dispatched_at": task.dispatched_at.isoformat() if task.dispatched_at else None,
  253. "started_at": task.started_at.isoformat() if task.started_at else None,
  254. "completed_at": task.completed_at.isoformat() if task.completed_at else None,
  255. "duration": task.duration,
  256. }
  257. if task.progress_data:
  258. try:
  259. result["progress_data"] = json.loads(task.progress_data)
  260. except (json.JSONDecodeError, Exception):
  261. result["progress_data"] = None
  262. if task.result_metrics:
  263. try:
  264. result["result_metrics"] = json.loads(task.result_metrics)
  265. except (json.JSONDecodeError, Exception):
  266. result["result_metrics"] = None
  267. return result
  268. # Global singleton
  269. _task_manager: Optional[TaskManager] = None
  270. def get_task_manager() -> TaskManager:
  271. """Get or create global TaskManager singleton."""
  272. global _task_manager
  273. if _task_manager is None:
  274. _task_manager = TaskManager()
  275. return _task_manager