flow_controller.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import logging
  2. import time
  3. from concurrent.futures import ThreadPoolExecutor
  4. from typing import Any
  5. from fastapi import APIRouter, Request
  6. from app.config import settings
  7. from app.repository.record_interface import RecordInterface
  8. from app.service.engine.workflow_engine import WorkflowEngine
  9. from app.service.spec.workflow_loader import load_from_directory
  10. from app.service.task.task_executor import TaskExecutor
  11. from app.service.template.template_service import TemplateService
  12. logger = logging.getLogger(__name__)
  13. router = APIRouter()
  14. # 全局单例(在 main.py 中初始化并注入)
  15. _record_store: RecordInterface | None = None
  16. _template_service: TemplateService | None = None
  17. _engine: WorkflowEngine | None = None
  18. _executor: ThreadPoolExecutor | None = None
  19. def init_controller(
  20. record_store: RecordInterface,
  21. template_service: TemplateService,
  22. ):
  23. global _record_store, _template_service, _engine, _executor
  24. _record_store = record_store
  25. _template_service = template_service
  26. _engine = WorkflowEngine(TaskExecutor.create_default())
  27. _executor = ThreadPoolExecutor(
  28. max_workers=settings.executor_pool_size,
  29. thread_name_prefix="workflow-executor-",
  30. )
  31. logger.info("FlowController initialized with pool size: %d", settings.executor_pool_size)
  32. def shutdown_controller():
  33. global _executor
  34. if _executor:
  35. _executor.shutdown(wait=True, cancel_futures=False)
  36. logger.info("FlowController thread pool shut down")
  37. @router.post("/execute")
  38. async def execute(request: Request) -> dict[str, Any]:
  39. req: dict = await request.json()
  40. template_url = req.get("template")
  41. inputs: dict = req.get("inputs") if isinstance(req.get("inputs"), dict) else {}
  42. logger.info("POST /execute template=%s, input_keys=%s", template_url, list(inputs.keys()))
  43. if not template_url or not str(template_url).strip():
  44. logger.warning("POST /execute rejected: template parameter is required")
  45. return {"state": "failed", "message": "template parameter is required"}
  46. task_id = _record_store.create_record(req)
  47. logger.info("[%s] Task accepted, submitting to thread pool", task_id)
  48. def _run():
  49. t0 = time.time()
  50. try:
  51. _record_store.set_processing(task_id)
  52. logger.info("[%s] Loading template: %s", task_id, template_url)
  53. try:
  54. template_path = _template_service.get_template_path(template_url)
  55. flow = load_from_directory(template_path)
  56. logger.info("[%s] Workflow loaded: version=%s, steps=%s, order=%s",
  57. task_id, flow.version, len(flow.steps), flow.order)
  58. except Exception as e:
  59. logger.error("[%s] Failed to load workflow: %s", task_id, e)
  60. _record_store.set_failed(task_id, f"Failed to load workflow: {e}")
  61. return
  62. logger.info("[%s] Starting workflow execution", task_id)
  63. result = _engine.execute(task_id, flow, inputs)
  64. elapsed = time.time() - t0
  65. if result.success:
  66. logger.info("[%s] Workflow completed successfully in %.2fs", task_id, elapsed)
  67. _record_store.set_success(task_id, result.outputs)
  68. else:
  69. logger.warning("[%s] Workflow failed in %.2fs: %s", task_id, elapsed, result.error_message)
  70. _record_store.set_failed(task_id, result.error_message)
  71. except Exception as e:
  72. elapsed = time.time() - t0
  73. logger.error("[%s] Workflow execution exception in %.2fs: %s", task_id, elapsed, e, exc_info=True)
  74. _record_store.set_failed(task_id, str(e))
  75. _executor.submit(_run)
  76. return {"state": "successes", "taskId": task_id}
  77. @router.get("/result")
  78. async def result(taskId: str) -> dict[str, Any]:
  79. logger.info("GET /result taskId=%s", taskId)
  80. r = _record_store.get_record(taskId)
  81. if r is None:
  82. logger.warning("GET /result taskId=%s not found", taskId)
  83. return {"error": "task not found"}
  84. return {
  85. "taskId": r.task_id,
  86. "status": r.status,
  87. "message": r.message,
  88. "type": r.type,
  89. "data": r.data,
  90. "request": r.request or {},
  91. "createTime": r.create_time,
  92. "updateTime": r.update_time,
  93. }
  94. @router.get("/tasks")
  95. async def get_all_tasks() -> dict[str, Any]:
  96. all_tasks = _record_store.get_all_records()
  97. return {
  98. "total": len(all_tasks),
  99. "tasks": {tid: rec.to_dict() for tid, rec in all_tasks.items()},
  100. }
  101. @router.post("/cleanup")
  102. async def cleanup_tasks(request: Request) -> dict[str, Any]:
  103. req: dict = await request.json()
  104. max_age = int(req.get("maxAgeHours", 24))
  105. _record_store.delete_records(max_age)
  106. return {"message": "Cleanup completed", "maxAgeHours": max_age}
  107. @router.get("/health")
  108. async def health() -> dict[str, Any]:
  109. return {
  110. "status": "UP",
  111. "service": settings.app_name,
  112. "timestamp": int(time.time() * 1000),
  113. }
  114. @router.get("/executor/status")
  115. async def executor_status() -> dict[str, Any]:
  116. if _executor is None:
  117. return {"error": "executor not initialized"}
  118. return {
  119. "maxWorkers": _executor._max_workers,
  120. "isShutdown": _executor._shutdown,
  121. }
  122. @router.get("/template/cache/status")
  123. async def template_cache_status() -> dict[str, Any]:
  124. return _template_service.get_cache_stats()
  125. @router.post("/template/cache/cleanup")
  126. async def template_cache_cleanup(request: Request) -> dict[str, Any]:
  127. req: dict = await request.json()
  128. max_age = int(req.get("maxAgeHours", 24))
  129. _template_service.cleanup_expired_templates(max_age)
  130. return {"message": "Template cache cleanup completed", "maxAgeHours": max_age}