manager.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270
  1. # Copyright (C) 2025 AIDC-AI
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. # http://www.apache.org/licenses/LICENSE-2.0
  7. # Unless required by applicable law or agreed to in writing, software
  8. # distributed under the License is distributed on an "AS IS" BASIS,
  9. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  10. # See the License for the specific language governing permissions and
  11. # limitations under the License.
  12. """
  13. Task Manager
  14. In-memory task management for video generation jobs.
  15. """
  16. import asyncio
  17. import uuid
  18. from datetime import datetime, timedelta
  19. from typing import Dict, List, Optional, Callable
  20. from loguru import logger
  21. from api.tasks.models import Task, TaskStatus, TaskType, TaskProgress
  22. from api.config import api_config
  23. class TaskManager:
  24. """
  25. Task manager for handling async video generation tasks
  26. Features:
  27. - In-memory storage (can be replaced with Redis later)
  28. - Task lifecycle management
  29. - Progress tracking
  30. - Auto cleanup of old tasks
  31. """
  32. def __init__(self):
  33. self._tasks: Dict[str, Task] = {}
  34. self._task_futures: Dict[str, asyncio.Task] = {}
  35. self._cleanup_task: Optional[asyncio.Task] = None
  36. self._running = False
  37. async def start(self):
  38. """Start task manager and cleanup scheduler"""
  39. if self._running:
  40. logger.warning("Task manager already running")
  41. return
  42. self._running = True
  43. self._cleanup_task = asyncio.create_task(self._cleanup_loop())
  44. logger.info("✅ Task manager started")
  45. async def stop(self):
  46. """Stop task manager and cancel all tasks"""
  47. self._running = False
  48. # Cancel cleanup task
  49. if self._cleanup_task:
  50. self._cleanup_task.cancel()
  51. try:
  52. await self._cleanup_task
  53. except asyncio.CancelledError:
  54. pass
  55. # Cancel all running tasks
  56. for task_id, future in self._task_futures.items():
  57. if not future.done():
  58. future.cancel()
  59. logger.info(f"Cancelled task: {task_id}")
  60. self._tasks.clear()
  61. self._task_futures.clear()
  62. logger.info("✅ Task manager stopped")
  63. def create_task(
  64. self,
  65. task_type: TaskType,
  66. request_params: Optional[dict] = None
  67. ) -> Task:
  68. """
  69. Create a new task
  70. Args:
  71. task_type: Type of task
  72. request_params: Original request parameters
  73. Returns:
  74. Created task
  75. """
  76. task_id = str(uuid.uuid4())
  77. task = Task(
  78. task_id=task_id,
  79. task_type=task_type,
  80. status=TaskStatus.PENDING,
  81. request_params=request_params,
  82. )
  83. self._tasks[task_id] = task
  84. logger.info(f"Created task {task_id} ({task_type})")
  85. return task
  86. async def execute_task(
  87. self,
  88. task_id: str,
  89. coro_func: Callable,
  90. *args,
  91. **kwargs
  92. ):
  93. """
  94. Execute task asynchronously
  95. Args:
  96. task_id: Task ID
  97. coro_func: Async function to execute
  98. *args: Positional arguments
  99. **kwargs: Keyword arguments
  100. """
  101. task = self._tasks.get(task_id)
  102. if not task:
  103. logger.error(f"Task {task_id} not found")
  104. return
  105. # Create async task
  106. async def _execute():
  107. try:
  108. task.status = TaskStatus.RUNNING
  109. task.started_at = datetime.now()
  110. logger.info(f"Task {task_id} started")
  111. # Execute the actual work
  112. result = await coro_func(*args, **kwargs)
  113. # Update task with result
  114. task.status = TaskStatus.COMPLETED
  115. task.result = result
  116. task.completed_at = datetime.now()
  117. logger.info(f"Task {task_id} completed")
  118. except Exception as e:
  119. task.status = TaskStatus.FAILED
  120. task.error = str(e)
  121. task.completed_at = datetime.now()
  122. logger.error(f"Task {task_id} failed: {e}")
  123. # Start execution
  124. future = asyncio.create_task(_execute())
  125. self._task_futures[task_id] = future
  126. def get_task(self, task_id: str) -> Optional[Task]:
  127. """Get task by ID"""
  128. return self._tasks.get(task_id)
  129. def list_tasks(
  130. self,
  131. status: Optional[TaskStatus] = None,
  132. limit: int = 100
  133. ) -> List[Task]:
  134. """
  135. List tasks with optional filtering
  136. Args:
  137. status: Filter by status
  138. limit: Maximum number of tasks to return
  139. Returns:
  140. List of tasks
  141. """
  142. tasks = list(self._tasks.values())
  143. if status:
  144. tasks = [t for t in tasks if t.status == status]
  145. # Sort by created_at descending
  146. tasks.sort(key=lambda t: t.created_at, reverse=True)
  147. return tasks[:limit]
  148. def update_progress(
  149. self,
  150. task_id: str,
  151. current: int,
  152. total: int,
  153. message: str = ""
  154. ):
  155. """
  156. Update task progress
  157. Args:
  158. task_id: Task ID
  159. current: Current progress
  160. total: Total steps
  161. message: Progress message
  162. """
  163. task = self._tasks.get(task_id)
  164. if not task:
  165. return
  166. percentage = (current / total * 100) if total > 0 else 0
  167. task.progress = TaskProgress(
  168. current=current,
  169. total=total,
  170. percentage=percentage,
  171. message=message
  172. )
  173. def cancel_task(self, task_id: str) -> bool:
  174. """
  175. Cancel a running task
  176. Args:
  177. task_id: Task ID
  178. Returns:
  179. True if cancelled, False otherwise
  180. """
  181. task = self._tasks.get(task_id)
  182. if not task:
  183. return False
  184. # Do not cancel already-terminal tasks
  185. if task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]:
  186. return False
  187. # Cancel future if running
  188. future = self._task_futures.get(task_id)
  189. if future and not future.done():
  190. future.cancel()
  191. # Update task status
  192. task.status = TaskStatus.CANCELLED
  193. task.completed_at = datetime.now()
  194. logger.info(f"Cancelled task {task_id}")
  195. return True
  196. async def _cleanup_loop(self):
  197. """Periodically clean up old completed tasks"""
  198. while self._running:
  199. try:
  200. await asyncio.sleep(api_config.task_cleanup_interval)
  201. self._cleanup_old_tasks()
  202. except asyncio.CancelledError:
  203. break
  204. except Exception as e:
  205. logger.error(f"Error in cleanup loop: {e}")
  206. def _cleanup_old_tasks(self):
  207. """Remove old completed/failed tasks"""
  208. cutoff_time = datetime.now() - timedelta(seconds=api_config.task_retention_time)
  209. tasks_to_remove = []
  210. for task_id, task in self._tasks.items():
  211. if task.status in [TaskStatus.COMPLETED, TaskStatus.FAILED, TaskStatus.CANCELLED]:
  212. if task.completed_at and task.completed_at < cutoff_time:
  213. tasks_to_remove.append(task_id)
  214. for task_id in tasks_to_remove:
  215. del self._tasks[task_id]
  216. if task_id in self._task_futures:
  217. del self._task_futures[task_id]
  218. if tasks_to_remove:
  219. logger.info(f"Cleaned up {len(tasks_to_remove)} old tasks")
  220. # Global task manager instance
  221. task_manager = TaskManager()