Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2f1f2d5a17 | ||
|
|
5c9c80fd72 | ||
|
|
f3ef1387ba |
30
README.md
30
README.md
@@ -1,8 +1,14 @@
|
||||
# xc_validation_strategy_vllm_stop
|
||||
|
||||
用于停止 `zhoukaile` 账号当前状态为 `waiting` 的验证任务的 ModelHub XC 策略服务。
|
||||
用于停止以下账号中状态严格为 `waiting` 的 ModelHub XC 验证任务:
|
||||
|
||||
服务启动后会调用:
|
||||
- `fanyi`:100 条
|
||||
- `jiajing`:88 条
|
||||
- `i-zhouyuanxi@4paradigm.com`:94 条
|
||||
|
||||
任务清单于 2026-07-26 通过 `/api/adapt/task/page` 分页查询生成,共 282 条。`success`、`failed` 等其他状态不会写入清单,也不会被停止。
|
||||
|
||||
服务启动后按账号及每批 50 条调用:
|
||||
|
||||
```text
|
||||
PUT /api/async/task/stop-create-contest-task
|
||||
@@ -11,20 +17,10 @@ PUT /api/async/task/stop-create-contest-task
|
||||
|
||||
停止请求完成后,进程继续运行,以便平台通过健康检查和状态接口读取执行结果。
|
||||
|
||||
## 必填配置
|
||||
|
||||
在策略构建/运行环境中配置以下机密环境变量:
|
||||
|
||||
```text
|
||||
XC_TOKEN=<zhoukaile 的 xc-Token>
|
||||
USER_PASSWORD=<zhoukaile 的登录密码>
|
||||
```
|
||||
|
||||
`USER_PASSWORD` 仅用于获得短期 Bearer Token,以分页读取该账号实时提交的验证任务;`XC_TOKEN` 用于停止接口。两者均不写入仓库。可选配置如下:
|
||||
## 可选配置
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `TASK_IDS` | 无 | 可选的逗号分隔任务 ID;设置后跳过实时查询,仅停止指定 ID。 |
|
||||
| `BASE_URL` | `https://modelhub.org.cn` | ModelHub 服务地址。 |
|
||||
| `BATCH_SIZE` | `50` | 每个停止请求包含的任务数。 |
|
||||
| `MAX_RETRIES` | `3` | 单批任务的最大请求次数。 |
|
||||
@@ -34,17 +30,13 @@ USER_PASSWORD=<zhoukaile 的登录密码>
|
||||
## 平台运行接口
|
||||
|
||||
- `GET /health`:存活探针,成功返回 `{"status":"ok"}`。
|
||||
- `GET /status`:返回执行阶段、成功/失败数量、失败的任务 ID 与错误信息。
|
||||
- `GET /status`:返回总体及每个账号的成功/失败数量、失败任务 ID 和错误信息。
|
||||
|
||||
## 运行与构建
|
||||
|
||||
仓库根目录的 `Dockerfile` 使用平台 Python 基础镜像,暴露 `8080` 端口并以 `python main.py` 启动。策略构建时请将 `XC_TOKEN` 与 `USER_PASSWORD` 都设置为机密变量;构建成功后,策略会先分页读取当前账号的全部任务,仅筛选状态为 `waiting` 的任务再分批停止。
|
||||
|
||||
本地验证:
|
||||
仓库根目录的 `Dockerfile` 使用平台 Python 基础镜像,暴露 `8080` 端口并以 `python main.py` 启动。构建成功后,策略会依次处理三个账号的内置任务清单。
|
||||
|
||||
```bash
|
||||
export XC_TOKEN='***'
|
||||
export USER_PASSWORD='***'
|
||||
python main.py
|
||||
curl http://127.0.0.1:8080/health
|
||||
curl http://127.0.0.1:8080/status
|
||||
|
||||
214
main.py
214
main.py
@@ -1,4 +1,4 @@
|
||||
"""Stop zhoukaile's waiting ModelHub XC validation tasks.
|
||||
"""Stop waiting ModelHub XC validation tasks for the configured accounts.
|
||||
|
||||
The service performs the stop requests once at startup and then stays alive so
|
||||
the strategy platform can probe it through ``/health`` and inspect ``/status``.
|
||||
@@ -16,12 +16,7 @@ from typing import Any
|
||||
import requests
|
||||
|
||||
BASE_URL = os.environ.get("BASE_URL", "https://modelhub.org.cn").rstrip("/")
|
||||
LOGIN_ENDPOINT = "/adminApi/user/login"
|
||||
TASK_PAGE_ENDPOINT = "/api/adapt/task/page"
|
||||
STOP_TASK_ENDPOINT = "/api/async/task/stop-create-contest-task"
|
||||
USER_ACCOUNT = os.environ.get("USER_ACCOUNT", "zhoukaile")
|
||||
USER_PASSWORD = os.environ.get("USER_PASSWORD", "")
|
||||
XC_TOKEN = os.environ.get("XC_TOKEN", "")
|
||||
STRATEGY_ID = os.environ.get("STRATEGY_ID", "")
|
||||
|
||||
HTTP_HOST = "0.0.0.0"
|
||||
@@ -30,15 +25,85 @@ BATCH_SIZE = int(os.environ.get("BATCH_SIZE", "50"))
|
||||
MAX_RETRIES = int(os.environ.get("MAX_RETRIES", "3"))
|
||||
REQUEST_TIMEOUT = int(os.environ.get("REQUEST_TIMEOUT", "30"))
|
||||
|
||||
PAGE_SIZE = 100
|
||||
STOPPABLE_STATUS = "waiting"
|
||||
# Snapshot queried from /api/adapt/task/page on 2026-07-26. Only records
|
||||
# whose status was exactly "waiting" are included.
|
||||
TARGETS = [
|
||||
{
|
||||
"account": "fanyi",
|
||||
"xc_token": "f2d501c9ae6543a589cd6cb789108c41",
|
||||
"task_ids": [
|
||||
3469928, 3469927, 3469926, 3469925, 3469924, 3469923, 3469922,
|
||||
3469921, 3469920, 3469919, 3469918, 3469917, 3469916, 3469915,
|
||||
3469914, 3469913, 3469911, 3469910, 3469909, 3469908, 3469907,
|
||||
3469905, 3469904, 3469903, 3469902, 3469901, 3469900, 3469899,
|
||||
3469898, 3469897, 3469896, 3469895, 3469894, 3469893, 3469892,
|
||||
3469891, 3469890, 3469889, 3469888, 3469887, 3469886, 3469885,
|
||||
3469884, 3469883, 3469882, 3469881, 3469879, 3469878, 3469877,
|
||||
3469875, 3469874, 3469873, 3469872, 3469871, 3469870, 3469869,
|
||||
3469868, 3469867, 3469866, 3469865, 3469864, 3469863, 3469862,
|
||||
3469861, 3469860, 3469859, 3469858, 3469857, 3469856, 3469855,
|
||||
3469854, 3469853, 3469852, 3469851, 3469850, 3469849, 3469848,
|
||||
3469847, 3469846, 3469845, 3469844, 3469843, 3469842, 3469841,
|
||||
3469840, 3469837, 3469836, 3469835, 3469834, 3469833, 3469832,
|
||||
3469831, 3469830, 3469829, 3469828, 3469827, 3469826, 3469825,
|
||||
3469822, 3469821,
|
||||
],
|
||||
},
|
||||
{
|
||||
"account": "jiajing",
|
||||
"xc_token": "5e051e0ff8384a81af53bea780deb28a",
|
||||
"task_ids": [
|
||||
3469551, 3469550, 3469549, 3469548, 3469547, 3469546, 3469545,
|
||||
3469544, 3469543, 3469542, 3469541, 3469540, 3469539, 3469538,
|
||||
3469537, 3469536, 3469535, 3469534, 3469533, 3469532, 3469531,
|
||||
3469530, 3469529, 3469528, 3469526, 3469525, 3469524, 3469523,
|
||||
3469522, 3469521, 3469520, 3469519, 3469518, 3469517, 3469516,
|
||||
3469515, 3469514, 3469513, 3469512, 3469511, 3469510, 3469508,
|
||||
3469507, 3469503, 3469501, 3469500, 3469499, 3469497, 3469495,
|
||||
3469493, 3469492, 3469491, 3469490, 3469488, 3469486, 3469485,
|
||||
3469484, 3469483, 3469482, 3469480, 3469477, 3469476, 3469475,
|
||||
3469474, 3469472, 3469471, 3469470, 3469469, 3469468, 3469467,
|
||||
3469466, 3469465, 3469464, 3469463, 3469462, 3469461, 3469460,
|
||||
3469459, 3469458, 3469457, 3469456, 3469455, 3469454, 3469453,
|
||||
3469452, 3469451, 3469450, 3469449,
|
||||
],
|
||||
},
|
||||
{
|
||||
"account": "i-zhouyuanxi@4paradigm.com",
|
||||
"xc_token": "62b9b487eff2488fb9f1da0b963f0b93",
|
||||
"task_ids": [
|
||||
3450582, 3450581, 3450580, 3450579, 3450575, 3450573, 3450572,
|
||||
3450570, 3450569, 3450568, 3450567, 3450565, 3450560, 3450557,
|
||||
3450556, 3450555, 3450554, 3450553, 3450552, 3450551, 3450550,
|
||||
3450549, 3450548, 3450544, 3450541, 3450538, 3450537, 3450536,
|
||||
3450535, 3450534, 3450533, 3450532, 3450531, 3450530, 3450529,
|
||||
3450528, 3450527, 3450526, 3450525, 3450524, 3450523, 3450522,
|
||||
3450521, 3450520, 3450519, 3450518, 3450517, 3450514, 3450513,
|
||||
3450512, 3450511, 3450510, 3450509, 3450508, 3450507, 3450506,
|
||||
3450505, 3450503, 3450501, 3450498, 3450496, 3450493, 3450483,
|
||||
3450481, 3450480, 3450479, 3450478, 3450477, 3450476, 3450475,
|
||||
3450474, 3450473, 3450472, 3450471, 3450470, 3450469, 3450463,
|
||||
3450458, 3450455, 3450451, 3450441, 3450440, 3450439, 3450438,
|
||||
3450437, 3450436, 3450435, 3450434, 3450433, 3450432, 3450431,
|
||||
3450430, 3450429, 3450428,
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
_shutdown = threading.Event()
|
||||
_state: dict[str, Any] = {
|
||||
"strategy_id": STRATEGY_ID,
|
||||
"account": USER_ACCOUNT,
|
||||
"accounts": {
|
||||
target["account"]: {
|
||||
"total": len(target["task_ids"]),
|
||||
"stopped": 0,
|
||||
"failed": 0,
|
||||
"failed_task_ids": [],
|
||||
}
|
||||
for target in TARGETS
|
||||
},
|
||||
"phase": "starting", # starting | stopping | done | partial_failure | error
|
||||
"total": 0,
|
||||
"total": sum(len(target["task_ids"]) for target in TARGETS),
|
||||
"stopped": 0,
|
||||
"failed": 0,
|
||||
"failed_task_ids": [],
|
||||
@@ -52,83 +117,6 @@ def _now() -> str:
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
|
||||
def _task_ids_from_environment() -> list[int] | None:
|
||||
"""Return an explicit TASK_IDS override, if one was supplied."""
|
||||
raw_task_ids = os.environ.get("TASK_IDS", "").strip()
|
||||
if not raw_task_ids:
|
||||
return None
|
||||
|
||||
task_ids: list[int] = []
|
||||
for value in raw_task_ids.split(","):
|
||||
value = value.strip()
|
||||
if not value:
|
||||
continue
|
||||
try:
|
||||
task_ids.append(int(value))
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"TASK_IDS contains a non-numeric task ID: {value!r}") from exc
|
||||
return list(dict.fromkeys(task_ids))
|
||||
|
||||
|
||||
def _login() -> str:
|
||||
"""Authenticate as the task owner and return a short-lived bearer token."""
|
||||
if not USER_PASSWORD:
|
||||
raise RuntimeError("USER_PASSWORD is required to query zhoukaile's current task IDs")
|
||||
response = requests.post(
|
||||
f"{BASE_URL}{LOGIN_ENDPOINT}",
|
||||
headers={"Content-Type": "application/json"},
|
||||
json={"userAccount": USER_ACCOUNT, "userPassword": USER_PASSWORD},
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
token = result.get("data", {}).get("token")
|
||||
if result.get("code") != 0 or not isinstance(token, str) or not token:
|
||||
raise RuntimeError(f"Task query login failed: {result.get('message', 'missing token')}")
|
||||
return token
|
||||
|
||||
|
||||
def _current_task_ids() -> list[int]:
|
||||
"""Fetch every waiting task currently submitted by USER_ACCOUNT.
|
||||
|
||||
This intentionally queries the user-facing task page at run time. A static
|
||||
list becomes obsolete as soon as zhoukaile submits more validation tasks.
|
||||
"""
|
||||
bearer_token = _login()
|
||||
headers = {"Authorization": f"Bearer {bearer_token}"}
|
||||
task_ids: list[int] = []
|
||||
page = 1
|
||||
|
||||
while True:
|
||||
response = requests.get(
|
||||
f"{BASE_URL}{TASK_PAGE_ENDPOINT}",
|
||||
headers=headers,
|
||||
params={"current": page, "pageSize": PAGE_SIZE},
|
||||
timeout=REQUEST_TIMEOUT,
|
||||
)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
if result.get("code") != 0:
|
||||
raise RuntimeError(f"Task query failed: {result.get('message', 'unknown error')}")
|
||||
|
||||
data = result.get("data") or {}
|
||||
records = data.get("records") or []
|
||||
for record in records:
|
||||
if str(record.get("status", "")).lower() != STOPPABLE_STATUS:
|
||||
continue
|
||||
try:
|
||||
task_ids.append(int(record["taskId"]))
|
||||
except (KeyError, TypeError, ValueError):
|
||||
print(f"[query] skipping record without a numeric taskId: {record}", flush=True)
|
||||
|
||||
pages = int(data.get("pages") or 0)
|
||||
if page >= pages:
|
||||
break
|
||||
page += 1
|
||||
|
||||
return list(dict.fromkeys(task_ids))
|
||||
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
|
||||
if self.path == "/health":
|
||||
@@ -159,8 +147,11 @@ def _run_http() -> None:
|
||||
server.server_close()
|
||||
|
||||
|
||||
def _stop_batch(task_ids: list[int]) -> bool:
|
||||
headers = {"Content-Type": "application/json", "xc-Token": XC_TOKEN}
|
||||
def _stop_batch(account: str, xc_token: str, task_ids: list[int]) -> bool:
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"xc-Token": xc_token,
|
||||
}
|
||||
url = f"{BASE_URL}{STOP_TASK_ENDPOINT}"
|
||||
for attempt in range(1, MAX_RETRIES + 1):
|
||||
try:
|
||||
@@ -176,15 +167,19 @@ def _stop_batch(task_ids: list[int]) -> bool:
|
||||
result = {"message": response.text[:500]}
|
||||
|
||||
if response.ok and result.get("code") == 0:
|
||||
print(f"[stop] stopped task IDs: {task_ids}", flush=True)
|
||||
print(f"[stop] {account}: stopped task IDs: {task_ids}", flush=True)
|
||||
return True
|
||||
print(
|
||||
f"[stop] attempt {attempt}/{MAX_RETRIES} failed for {task_ids}: "
|
||||
f"[stop] {account}: attempt {attempt}/{MAX_RETRIES} failed for {task_ids}: "
|
||||
f"HTTP {response.status_code}, {result}",
|
||||
flush=True,
|
||||
)
|
||||
except requests.RequestException as exc:
|
||||
print(f"[stop] attempt {attempt}/{MAX_RETRIES} request error for {task_ids}: {exc}", flush=True)
|
||||
print(
|
||||
f"[stop] {account}: attempt {attempt}/{MAX_RETRIES} "
|
||||
f"request error for {task_ids}: {exc}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if attempt < MAX_RETRIES and not _shutdown.wait(attempt):
|
||||
continue
|
||||
@@ -197,25 +192,22 @@ def _run_worker() -> None:
|
||||
_state["started_at"] = _now()
|
||||
_state["phase"] = "stopping"
|
||||
try:
|
||||
if not XC_TOKEN:
|
||||
raise RuntimeError("XC_TOKEN is required and must be configured in the strategy environment")
|
||||
|
||||
task_ids = _task_ids_from_environment() or _current_task_ids()
|
||||
if not task_ids:
|
||||
print("[query] no active validation tasks found", flush=True)
|
||||
_state["phase"] = "done"
|
||||
return
|
||||
_state["total"] = len(task_ids)
|
||||
|
||||
for start in range(0, len(task_ids), BATCH_SIZE):
|
||||
if _shutdown.is_set():
|
||||
break
|
||||
batch = task_ids[start : start + BATCH_SIZE]
|
||||
if _stop_batch(batch):
|
||||
_state["stopped"] += len(batch)
|
||||
else:
|
||||
_state["failed"] += len(batch)
|
||||
_state["failed_task_ids"].extend(batch)
|
||||
for target in TARGETS:
|
||||
account = target["account"]
|
||||
account_state = _state["accounts"][account]
|
||||
task_ids = target["task_ids"]
|
||||
for start in range(0, len(task_ids), BATCH_SIZE):
|
||||
if _shutdown.is_set():
|
||||
break
|
||||
batch = task_ids[start : start + BATCH_SIZE]
|
||||
if _stop_batch(account, target["xc_token"], batch):
|
||||
_state["stopped"] += len(batch)
|
||||
account_state["stopped"] += len(batch)
|
||||
else:
|
||||
_state["failed"] += len(batch)
|
||||
_state["failed_task_ids"].extend(batch)
|
||||
account_state["failed"] += len(batch)
|
||||
account_state["failed_task_ids"].extend(batch)
|
||||
|
||||
_state["phase"] = "done" if _state["failed"] == 0 else "partial_failure"
|
||||
except Exception as exc: # exposed through /status for diagnosis
|
||||
|
||||
Reference in New Issue
Block a user