cloud: worker dispatch, DB abstraction, packaging 0.1.0, repo CI workflow
This commit is contained in:
1 parent
076ba01c74
commit
07ef869267
13 files changed
+1916
-9
No files matched your search
+105
-2
@@ -9,6 +9,8 @@
|
||||
- CloudExecutor:占位,未接通云端执行。
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
@@ -50,20 +52,121 @@ class LocalExecutor(Executor):
|
||||
runner 默认是 subprocess.run 的薄封装;单测可注入假 runner,假
|
||||
runner 返回 CompletedProcess 风格的 result 对象(含 returncode、
|
||||
stdout),本类只读这两个字段,不依赖其它字段。
|
||||
|
||||
execute() 之外另有 dispatch_task()/run_task():它们是 worker
|
||||
派发时调 host.py 真实子命令的入口——dispatch 对应 host.py 的
|
||||
dispatch(建任务并跑一轮 turn),run 对应 host.py 的 run
|
||||
(在已有 host 任务上再跑一轮 turn)。两者都只发真实存在的
|
||||
host.py 子命令,不编造命令名;execute() 本身仍只发只读 status,
|
||||
绝不在 execute 里直接跑模型回合(见 test_queue_sandbox 的断言)。
|
||||
"""
|
||||
|
||||
def __init__(self, host_py_path, config_path=None, runner=None):
|
||||
def __init__(self, host_py_path, config_path=None, runner=None,
|
||||
repo=None):
|
||||
self.host_py_path = host_py_path
|
||||
self.config_path = config_path
|
||||
self.runner = runner if runner is not None else _default_runner
|
||||
self.repo = repo
|
||||
|
||||
def _command(self, task_id):
|
||||
def _base_command(self):
|
||||
cmd = [sys.executable, self.host_py_path]
|
||||
if self.config_path is not None:
|
||||
cmd += ["--config", self.config_path]
|
||||
return cmd
|
||||
|
||||
def _command(self, task_id):
|
||||
cmd = self._base_command()
|
||||
cmd += ["status", "--task", str(task_id)]
|
||||
return cmd
|
||||
|
||||
def _result_from_runner(self, task_id, result):
|
||||
"""把 runner 返回值统一成 execute() 同款结果 dict。
|
||||
|
||||
host.py dispatch/run 成功时打印 JSON(含 status/text),
|
||||
失败时以 SystemExit(1) 退出并打印错误文本。因此:returncode
|
||||
非 0 一律 failed;returncode 为 0 时优先取 JSON 里的 status
|
||||
(completed 外的状态,如 timeout,均按失败计,worker 侧再
|
||||
判断是否可 fallback),取不到 JSON 时按 completed 计。
|
||||
"""
|
||||
returncode = getattr(result, "returncode", 1)
|
||||
stdout = _truncate_stdout(getattr(result, "stdout", ""))
|
||||
status = "failed"
|
||||
if returncode == 0:
|
||||
status = "completed"
|
||||
try:
|
||||
parsed = json.loads(stdout)
|
||||
except (TypeError, ValueError):
|
||||
parsed = None
|
||||
if isinstance(parsed, dict) and parsed.get("status"):
|
||||
status = ("completed" if parsed["status"] == "completed"
|
||||
else "failed")
|
||||
host_status = parsed["status"]
|
||||
else:
|
||||
host_status = None
|
||||
else:
|
||||
host_status = None
|
||||
return {
|
||||
"executor": "local",
|
||||
"task_id": task_id,
|
||||
"status": status,
|
||||
"stdout": stdout,
|
||||
"returncode": returncode,
|
||||
"host_status": host_status,
|
||||
# host.py 的 RuntimeError(如“任务已存在”)走 traceback 到
|
||||
# stderr,worker 判断 dispatch 续跑时需要它;execute()
|
||||
# 的旧结果 dict 不含此字段,保持不变。
|
||||
"stderr": _truncate_stdout(getattr(result, "stderr", "")),
|
||||
}
|
||||
|
||||
def _host_name(self, task):
|
||||
"""cloud 任务 id -> host 任务名:只保留安全字符。
|
||||
|
||||
server 生成的 id 本就是 uuid hex,走这一步只是防御自定义 id;
|
||||
同一 id 每次算出同一名字,dispatch 失败后的 run 续跑才能
|
||||
找到同一个 host 任务。
|
||||
"""
|
||||
raw = str(task.get("id"))
|
||||
return re.sub(r"[^A-Za-z0-9_.-]", "-", raw)
|
||||
|
||||
def dispatch_task(self, task) -> dict:
|
||||
"""调 host.py dispatch:建 host 任务并跑一轮 turn。
|
||||
|
||||
命令:host.py [--config CFG] dispatch --name <host 名>
|
||||
--task <prompt> [--repo <repo>]。repo 取任务的 repo 字段,
|
||||
没有则取构造时传的 repo,都没有则不传(此时 host.py 用其
|
||||
配置文件里的 repo,配置里也没有时 host.py 自己报错,
|
||||
worker 把该报错原样写回 failed,不编造成功)。
|
||||
"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
if not task.get("prompt"):
|
||||
raise ValueError("任务缺少 prompt")
|
||||
cmd = self._base_command()
|
||||
cmd += ["dispatch", "--name", self._host_name(task),
|
||||
"--task", task["prompt"]]
|
||||
repo = task.get("repo") or self.repo
|
||||
if repo:
|
||||
cmd += ["--repo", repo]
|
||||
return self._result_from_runner(
|
||||
task["id"], self.runner(cmd))
|
||||
|
||||
def run_task(self, task) -> dict:
|
||||
"""调 host.py run:在已有 host 任务上再跑一轮 turn。
|
||||
|
||||
命令:host.py [--config CFG] run --task <host 名>
|
||||
--prompt <prompt>。worker 只在 dispatch 报“任务已存在”时
|
||||
用它续跑,不用它做首次派发。
|
||||
"""
|
||||
if not isinstance(task, dict) or task.get("id") is None:
|
||||
raise ValueError("任务缺少 id")
|
||||
if not task.get("prompt"):
|
||||
raise ValueError("任务缺少 prompt")
|
||||
cmd = self._base_command()
|
||||
cmd += ["run", "--task", self._host_name(task),
|
||||
"--prompt", task["prompt"]]
|
||||
return self._result_from_runner(
|
||||
task["id"], self.runner(cmd))
|
||||
|
||||
def execute(self, task) -> dict:
|
||||
task_id = task.get("id") if isinstance(task, dict) else None
|
||||
if task_id is None:
|
||||
|
||||
Reference in new issue
Block a user