SchedulingSimulator/task/task_manager.py

279 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import time
import uuid
from datetime import datetime
from typing import Dict, List, Optional
import requests
import sys
import os
import importlib
from task.ai_task_submit import submit_ai_task
from config.db_manager import load_config_from_db, update_task_all_fields
# 添加TaskSubmissionScript到Python路径
sys.path.append(os.path.join(os.path.dirname(__file__), '..', 'TaskSubmissionScript'))
from task.hpc_task_submit import HPCTaskSubmitter
from config.database import load_db_config
from config.db_manager import update_cluster_resources
from config.config import logger, TASK_STATUS, task_map, task_map_lock
from config.cache_manager import get_cached_config
from task.cloud_task_submit import submit_cloud_task
def generate_task_templates() -> List[Dict]:
"""从数据库加载并返回任务模板"""
# 使用缓存的配置而不是每次都从数据库加载
config = get_cached_config()
return config.get("task_templates", [])
def load_tasks_to_queue(templates: List[Dict]) -> None:
with task_map_lock:
task_map.clear()
for idx, template in enumerate(templates):
try:
# 检查必填字段
required_fields = ["type", "task_name", "resource"]
missing_fields = [f for f in required_fields if f not in template]
if missing_fields:
logger.warning(f"跳过无效任务模板(索引{idx}):缺少字段 {missing_fields}")
continue
# 生成任务名称使用数据库中的task_name
task_name = template["task_name"] or f"task-{int(time.time())}"
task_type = template["type"]
resource = template["resource"]
strategy = template.get("strategy")
# 数据集ID设为可选
dataset_id = template.get("dataset_id", "")
code_id = template.get("code_id", "")
# 创建任务实例
task = {
"target_id": str(uuid.uuid4()), # 任务唯一ID
"task_name": task_name,
"package_name": f"{task_name.lower()}-pkg" if task_name else f"task-{int(time.time())}-pkg", # 文件夹名称
"type": task_type,
"status": TASK_STATUS["SUBMITTED"],
"submit_time": time.strftime("%Y-%m-%d %H:%M:%S"),
"success_time": None,
"third_party_task_id": "",
"dataset_id": dataset_id, # 数据集ID
"code_id":code_id,
"error_msg": "",
"fail_count": 0,
"max_fail_threshold": 3,
"cluster_id": "",
"strategy": strategy,
"resource": resource
}
# 根据任务类型设置特定字段
if task_type == "Ai":
# AI任务
task["dataset_id"] = dataset_id
task["resource"] = resource or {"CPU": 1, "MEMORY": 4, "NPU": 0}
# AI任务需要file_location字段但数据库中没有直接提供需要通过其他方式获取
elif task_type == "Hpc":
task["partition"] = "default" # 默认分区
task["ntasks"] = "1"
task["nodes"] = "1"
task["packageName"] = f"{task_name.lower()}-hpc" if task_name else f"task-{int(time.time())}-hpc"
task["hpcFile"] = "" # 数据库中没有提供
task["dataset_id"] = dataset_id
elif task_type == "Cloud":
task["replicas"] = 1
task["strategy"] = strategy or "resource" # 调度策略
task["cloud_strategy"] = "replication" # 部署策略
task["adapter_name"] = "" # 适配器名称
task["resource"] = resource or {"CPU": 0.1, "MEMORY": 6, "port": 80}
task["port"] = task["resource"].get("port", 80)
task["dataset_id"] = dataset_id
# 将任务添加到全局任务映射中
task_map[task["target_id"]] = task
logger.info(
f"任务入队 | task_name: {task_name} | 任务类型: {task_type} | "
f"{'含数据集' if dataset_id else '无数据集'}")
except Exception as e:
logger.error(f"处理任务模板时发生异常(索引{idx}: {e}", exc_info=True)
continue
logger.info(f"任务队列加载完成 | 总任务数: {len(task_map)}")
def get_task_submit_function(module_name):
"""动态导入任务提交函数"""
try:
module = importlib.import_module(module_name)
return getattr(module, 'main')
except (ImportError, AttributeError) as e:
logger.error(f"无法导入模块 {module_name} 或其 main 函数: {e}")
return None
def submit_single_task(task: Dict,token) -> bool:
"""提交单个任务到集群"""
# 获取选择的集群ID
global resource_changes
cluster_id = task.get("cluster_id")
if not cluster_id:
logger.error(f"[{task['task_name']}] 提交失败未指定集群ID")
return False
task_type = task["type"]
try:
if task_type == "Ai":
third_party_task_id = submit_ai_task(task,token)
elif task_type == "Hpc":
task_name = task["task_name"]
partition = task["partition"]
ntasks = task["ntasks"]
nodes = task["nodes"]
packageName = task["package_name"]
hpcFile = task.get("hpc_file", "") # 修正字段名使用hpcFile而非hpc_file
# 确定HPC任务类型根据任务名称推断
hpc_type = "bwa" # 默认类型
if "_bwa_" in task_name or "_bwa2_" in task_name:
hpc_type = "bwa"
elif "_hashcat_" in task_name:
hpc_type = "hashcat"
elif "_lammps_" in task_name:
hpc_type = "lammps"
# 创建HPC任务提交器实例
hpc_submitter = HPCTaskSubmitter()
# 提交HPC任务
third_party_task_id = hpc_submitter.submit_single_task(
task_name, hpc_type, partition, ntasks, nodes, packageName, hpcFile
)
# 如果返回的是字典格式提取jobSetID
if isinstance(third_party_task_id, dict) and "data" in third_party_task_id:
third_party_task_id = third_party_task_id["data"].get("jobSetID")
elif task_type == "Cloud":
# 提取云任务所需的参数
replicas = task.get("replicas")
task_res = task.get("resource")
cpu = task_res["CPU"]
memory = task_res["MEMORY"]
port = task.get("port")
task_name = task["task_name"]
cluster_id = task.get("cluster_id")
# 传递所有必要的参数给云任务提交函数
third_party_task_id = submit_cloud_task(
task_name,
cluster_id,
replicas=replicas,
cpu=cpu,
memory=memory,
port=port,
)
else:
raise ValueError(f"未知任务类型: {task_type}")
# 构造资源变化字典
task_resource = task.get("resource", {})
resource_changes = {}
# 计算资源变化量(负值表示扣减)
for res_type in ["CPU", "MEMORY"]:
required = task_resource.get(res_type, 0)
if required > 0:
resource_changes[res_type] = -required
# 处理加速器资源
for acc_type in [key for key in task_resource if key not in ["CPU", "MEMORY"]]:
required_count = task_resource.get(acc_type, 0)
if required_count > 0:
resource_changes[acc_type] = -required_count
if third_party_task_id:
# 使用批量操作更新数据库
if update_cluster_resources(cluster_id, resource_changes):
logger.info(f"成功从数据库扣减集群 {cluster_id} 的资源: {resource_changes}")
else:
logger.error(f"从数据库扣减集群 {cluster_id} 的资源失败: {resource_changes}")
# 更新任务状态为成功
logger.info(f"[{task['task_name']}] 任务提交至集群 {cluster_id} 成功 | 云际任务ID: {third_party_task_id}")
# 任务提交成功更新数据库中的状态为SUBMITTING
if update_task_all_fields(task['task_name'], status=TASK_STATUS['SUBMITTING'],third_party_task_id=third_party_task_id,commit_time= datetime.now()):
logger.info(f"成功更新任务 {task['task_name']} 在数据库中的状态为 {TASK_STATUS['SUBMITTING']}")
else:
logger.error(f"更新任务 {task['task_name']} 在数据库中的状态失败")
return False
return True
else:
# 如果任务提交失败且资源已更新,需要恢复资源
# 这里使用原子操作避免多次数据库访问
if resource_changes:
recovery_changes = {res: -change for res, change in resource_changes.items()}
update_cluster_resources(cluster_id, recovery_changes)
logger.info(f"任务失败,已从数据库恢复集群 {cluster_id} 的资源: {recovery_changes}")
raise ValueError("任务提交返回ID为空")
except Exception as e:
# 使用更高效的日志记录方式
logger.error(f"[{task['task_name']}] 提交失败: {str(e)}", exc_info=True, extra={
'cluster_id': cluster_id,
'task_type': task_type,
'resource_changes': resource_changes if 'resource_changes' in locals() else {}
})
return False
def query_third_party_task_status(third_party_task_id: str,token) -> Optional[Dict]:
"""查询云际平台任务状态"""
if not third_party_task_id:
logger.warning("云际任务ID为空无法查询状态")
return None
try:
config = load_config_from_db()
url = config["api_config"]["task_detail"]["url"]
params = {
"jobSetID": third_party_task_id, # 使用传入的任务ID作为参数
"localJobID": 1
}
headers = {"Authorization": f"Bearer {token}"} if token else {}
response = requests.get(
url,
params=params,
headers=headers,
timeout=config["api_config"]["task_detail"]["timeout"]
)
response.raise_for_status()
result = response.json()
# 适配新的接口返回结构,检查 code 是否为 "OK"
if result.get("code") != "OK":
logger.error(f"查询任务状态接口返回异常,响应码非 OK | 响应: {result}")
return None
# 检查 data 字段是否存在
if "data" not in result:
logger.error(f"查询任务状态接口返回异常,无 data 字段 | 响应: {result}")
return None
data = result["data"]
# 从数据库重新加载集群资源信息,确保获取最新的资源状态
load_db_config()
return data
except Exception as e:
logger.error(f"查询任务状态时发生异常: {str(e)}", exc_info=True)
return None