fix: unify detect effective job metrics
This commit is contained in:
127
docs/ops_center_runtime/HANDOFF_20260419_0240.md
Normal file
127
docs/ops_center_runtime/HANDOFF_20260419_0240.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# HANDOFF 2026-04-19 02:40
|
||||||
|
|
||||||
|
## 本轮完成了什么
|
||||||
|
|
||||||
|
本轮只处理了一个问题:
|
||||||
|
|
||||||
|
- Detect 主统计还是“中央原值 + display 附加值”双轨,Runtime / Detect 不够统一
|
||||||
|
|
||||||
|
实际完成:
|
||||||
|
|
||||||
|
1. 新增有效账本聚合逻辑
|
||||||
|
2. `active_job` 主字段切到统一后的有效值
|
||||||
|
3. 保留 `raw_*` 原始中央账本字段
|
||||||
|
4. 新增针对统一规则的后端单测
|
||||||
|
5. 重启 `domaincheck-api`
|
||||||
|
6. 重新验证线上接口
|
||||||
|
|
||||||
|
## 本轮关键结果
|
||||||
|
|
||||||
|
`detect/job/active` 现在主字段直接返回:
|
||||||
|
|
||||||
|
- `items_total = 1000`
|
||||||
|
- `items_pending = 931`
|
||||||
|
- `items_claimed = 34`
|
||||||
|
- `items_running = 14`
|
||||||
|
- `items_completed = 21`
|
||||||
|
|
||||||
|
同时保留原始中央账本:
|
||||||
|
|
||||||
|
- `raw_items_pending = 950`
|
||||||
|
- `raw_items_claimed = 25`
|
||||||
|
- `raw_items_running = 4`
|
||||||
|
|
||||||
|
`node_stats` / `distributed_node_stats` 当前已统一为:
|
||||||
|
|
||||||
|
- `mainland-controller-01`
|
||||||
|
- `mainland-worker-01`
|
||||||
|
- `overseas-control-01`
|
||||||
|
- `unassigned = 680`
|
||||||
|
|
||||||
|
`detect/queue-summary` 当前主字段:
|
||||||
|
|
||||||
|
- `pending = 931`
|
||||||
|
- `claimed = 34`
|
||||||
|
- `running = 14`
|
||||||
|
|
||||||
|
## 当前最高优先级
|
||||||
|
|
||||||
|
唯一最高优先级:
|
||||||
|
|
||||||
|
- 决定是否继续进入 item 级结果回写统一
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 当前主统计已经基本统一
|
||||||
|
- 剩下真正没闭环的是 `detect_result_projection -> detect_job_items`
|
||||||
|
|
||||||
|
## 当前状态结论
|
||||||
|
|
||||||
|
当前可以明确说:
|
||||||
|
|
||||||
|
- 检测能跑
|
||||||
|
- 大陆节点确实参与了
|
||||||
|
- Detect 页面和主统计都更接近真实执行态
|
||||||
|
- Runtime / Queue / Detect 主摘要开始吃同一套有效账本
|
||||||
|
|
||||||
|
## 当前唯一剩余问题
|
||||||
|
|
||||||
|
唯一剩余问题:
|
||||||
|
|
||||||
|
- 目前还是“聚合统一”
|
||||||
|
- 还不是“逐条 item 最终一致”
|
||||||
|
|
||||||
|
所以:
|
||||||
|
|
||||||
|
- 主观感受和主统计已经收口
|
||||||
|
- 底层逐条账本还没完全收口
|
||||||
|
|
||||||
|
## 下一轮如果继续,只做什么
|
||||||
|
|
||||||
|
下一轮只做这一件事:
|
||||||
|
|
||||||
|
- 查清是否要把 `detect_result_projection` 最小映射回 `detect_job_items`
|
||||||
|
|
||||||
|
优先原则:
|
||||||
|
|
||||||
|
- 不发散
|
||||||
|
- 不扩新页面
|
||||||
|
- 不碰发布动作
|
||||||
|
|
||||||
|
## 推荐模型与推理等级
|
||||||
|
|
||||||
|
继续推荐:
|
||||||
|
|
||||||
|
- `GPT-5.4`
|
||||||
|
- `high`
|
||||||
|
|
||||||
|
原因:
|
||||||
|
|
||||||
|
- 下一轮是 item 级账本映射策略
|
||||||
|
- 仍然是高复杂度,但还没到必须切超高
|
||||||
|
|
||||||
|
## 给下一轮的任务卡
|
||||||
|
|
||||||
|
```text
|
||||||
|
任务名:I1-detect_result_projection 到 detect_job_items 的最小映射设计批
|
||||||
|
|
||||||
|
目标:
|
||||||
|
1. 查清当前 detect_result_projection 能提供哪些聚合字段
|
||||||
|
2. 判断是否足以做 item 级状态回写,还是只能做批次级回写
|
||||||
|
3. 只做最小一致性设计,不重做执行链
|
||||||
|
|
||||||
|
范围限制:
|
||||||
|
1. 不新增页面
|
||||||
|
2. 不扩控制面功能
|
||||||
|
3. 不进入发布动作
|
||||||
|
4. 不并行做别的方向
|
||||||
|
|
||||||
|
完成判定:
|
||||||
|
1. 给出 item 级统一是否可行的结论
|
||||||
|
2. 如果可行,明确最小落点
|
||||||
|
3. 如果暂不可行,明确缺的字段和最小补数方案
|
||||||
|
```
|
||||||
|
|
||||||
|
## 一句话结论
|
||||||
|
|
||||||
|
这轮已经把 Detect 主统计从“靠 display 补丁看懂”推进到“主字段直接统一可读”;下一轮真正该做的,只剩下是否继续把结果同步推进到 item 级逐条账本。
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
# IMPLEMENTATION_STATUS
|
# IMPLEMENTATION_STATUS
|
||||||
|
|
||||||
更新时间:2026-04-19 02:33 CST
|
更新时间:2026-04-19 02:40 CST
|
||||||
|
|
||||||
## 当前真实状态
|
## 当前真实状态
|
||||||
|
|
||||||
阶段判断:
|
阶段判断:
|
||||||
|
|
||||||
- 海外单脑接管能力:约 `95%`
|
- 海外单脑接管能力:约 `95%`
|
||||||
- 分布式检测真实执行能力:约 `93%`
|
- 分布式检测真实执行能力:约 `94%`
|
||||||
- 距离“可稳定上线并放心用后台发起检测”:约 `92%`
|
- 距离“可稳定上线并放心用后台发起检测”:约 `93%~94%`
|
||||||
|
|
||||||
本轮最重要的新事实:
|
本轮最重要的新事实:
|
||||||
|
|
||||||
- Detect 页面已经不再只显示海外节点
|
- Detect 主统计口径已经从“双轨显示”推进到“统一后的有效账本”
|
||||||
- 大陆 controller / worker 已进入 Detect 总览显示口径
|
- `detect/job/active` 和 `detect/queue-summary` 主字段已直接体现大陆 + 海外的统一聚合结果
|
||||||
- 用户现在能从 Detect 页面直接看见大陆节点参与执行
|
- 原始中央账本仍完整保留,便于继续排障和审计
|
||||||
|
|
||||||
## 本轮代码收口
|
## 本轮代码收口
|
||||||
|
|
||||||
@@ -22,48 +22,64 @@
|
|||||||
|
|
||||||
- `domain-api/app/services/detect_job_service.py`
|
- `domain-api/app/services/detect_job_service.py`
|
||||||
|
|
||||||
本轮前端变更:
|
本轮测试变更:
|
||||||
|
|
||||||
- `domain-web/src/views/detect/DetectView.vue`
|
- `domain-api/tests/test_detect_job_service.py`
|
||||||
|
|
||||||
本轮修复方式:
|
本轮修复方式:
|
||||||
|
|
||||||
- 保留中央账本原值,不硬改 `detect_job_items`
|
- 保留中央 `detect_job_items` 原始值
|
||||||
- 额外补出分布式显示口径:
|
- 引入“有效统一账本”:
|
||||||
- `distributed_node_stats`
|
- `node_stats`
|
||||||
- `display_items_claimed`
|
- `items_pending`
|
||||||
- `display_items_running`
|
- `items_claimed`
|
||||||
- Detect 页面优先显示这个分布式显示口径
|
- `items_running`
|
||||||
|
- `items_completed`
|
||||||
|
- `items_failed`
|
||||||
|
- `items_terminal`
|
||||||
|
- 同时新增 `raw_*` 字段保留中央原始账本
|
||||||
|
|
||||||
## 关键验证证据
|
## 关键验证证据
|
||||||
|
|
||||||
接口验证:
|
接口验证:
|
||||||
|
|
||||||
- `detect/job/active`
|
- `detect/job/active`
|
||||||
- `display_items_claimed = 34`
|
- 已统一为:
|
||||||
- `display_items_running = 14`
|
- `items_total = 1000`
|
||||||
- `distributed_node_stats` 包含:
|
- `items_pending = 931`
|
||||||
|
- `items_claimed = 34`
|
||||||
|
- `items_running = 14`
|
||||||
|
- `items_completed = 21`
|
||||||
|
- 原始账本仍可追溯:
|
||||||
|
- `raw_items_pending = 950`
|
||||||
|
- `raw_items_claimed = 25`
|
||||||
|
- `raw_items_running = 4`
|
||||||
|
- `node_stats` 已包含:
|
||||||
- `mainland-controller-01`
|
- `mainland-controller-01`
|
||||||
- `mainland-worker-01`
|
- `mainland-worker-01`
|
||||||
- `overseas-control-01`
|
- `overseas-control-01`
|
||||||
|
- `unassigned`
|
||||||
- `detect/queue-summary`
|
- `detect/queue-summary`
|
||||||
- `queue.display_claimed = 34`
|
- 已统一为:
|
||||||
- `queue.display_running = 14`
|
- `pending = 931`
|
||||||
- `nodes` 已含大陆 controller / worker
|
- `claimed = 34`
|
||||||
|
- `running = 14`
|
||||||
|
|
||||||
编译验证:
|
编译验证:
|
||||||
|
|
||||||
- 后端:
|
- `python3 -m py_compile domain-api/app/services/detect_job_service.py domain-api/tests/test_detect_job_service.py`
|
||||||
- `python3 -m py_compile domain-api/app/services/detect_job_service.py`
|
|
||||||
- 通过
|
- 通过
|
||||||
- 前端:
|
|
||||||
- `cd domain-web && npm run build`
|
单测验证:
|
||||||
|
|
||||||
|
- `/opt/domaincheck/domainCheck/.venv/bin/python -m unittest tests.test_detect_job_service`
|
||||||
- 通过
|
- 通过
|
||||||
|
|
||||||
运行验证:
|
运行验证:
|
||||||
|
|
||||||
- `domaincheck-api` 已重启成功
|
- `domaincheck-api`
|
||||||
- 活跃接口返回新字段正常
|
- 已重启成功
|
||||||
|
- 线上接口返回统一账本字段正常
|
||||||
|
|
||||||
## 当前已闭合的问题
|
## 当前已闭合的问题
|
||||||
|
|
||||||
@@ -72,46 +88,43 @@
|
|||||||
- 大陆 controller 无法 `pull_tasks`
|
- 大陆 controller 无法 `pull_tasks`
|
||||||
- Detect 页面误判“大陆没跑”
|
- Detect 页面误判“大陆没跑”
|
||||||
- Detect 节点表格只显示海外与 `unassigned`
|
- Detect 节点表格只显示海外与 `unassigned`
|
||||||
|
- Detect 主统计仍依赖 display 附加字段才能看懂
|
||||||
|
|
||||||
## 当前剩余问题
|
## 当前剩余问题
|
||||||
|
|
||||||
当前唯一剩余问题:
|
当前唯一剩余问题:
|
||||||
|
|
||||||
- 中央账本与分布式显示口径还不是同一套最终账本
|
- 还没有做到 item 级逐条统一回写
|
||||||
|
|
||||||
具体来说:
|
具体来说:
|
||||||
|
|
||||||
- `detect_job_items`
|
- 当前已做到:
|
||||||
- 仍是中央派发账本
|
- 主统计口径一致
|
||||||
- `distributed_node_stats`
|
- 节点参与口径一致
|
||||||
- 是运行态 / 投影视角
|
- Runtime / Detect 读取同一套有效聚合值
|
||||||
|
- 当前未做到:
|
||||||
所以当前页面已经“可观察、可判断”了,但底层仍属于双账本阶段。
|
- 大陆执行结果逐条回写 `detect_job_items`
|
||||||
|
|
||||||
## 当前是否可以继续跑检测测试
|
## 当前是否可以继续跑检测测试
|
||||||
|
|
||||||
当前结论:
|
当前结论:
|
||||||
|
|
||||||
- 可以继续跑检测任务测试
|
- 可以继续跑检测任务测试
|
||||||
- 而且现在 Detect 页面已经能看出大陆节点参与
|
- 而且现在 Detect 主统计和节点参与都更接近真实执行态
|
||||||
|
|
||||||
## 当前是否建议直接上线
|
## 当前是否建议直接上线
|
||||||
|
|
||||||
当前结论:
|
当前结论:
|
||||||
|
|
||||||
- 比上一轮更接近上线
|
- 已经比上一轮更接近可上线状态
|
||||||
- 但还不建议把“检测调度账本”宣称为最终完全统一
|
- 如果目标是“后台可以稳定发起检测并看懂谁在跑”,已经基本可用
|
||||||
|
- 如果目标是“中央账本逐条结果完全统一”,还差最后一层 item 级回写
|
||||||
原因:
|
|
||||||
|
|
||||||
- 现在解决的是“显示正确性”
|
|
||||||
- 下一步要解决的是“账本统一性”
|
|
||||||
|
|
||||||
## 当前优先级判断
|
## 当前优先级判断
|
||||||
|
|
||||||
最高优先级:
|
最高优先级:
|
||||||
|
|
||||||
- 设计并收敛中央账本与分布式执行账本的统一规则
|
- 决定是否继续进入 `detect_result_projection -> detect_job_items` 的 item 级统一
|
||||||
|
|
||||||
当前不应继续推进:
|
当前不应继续推进:
|
||||||
|
|
||||||
@@ -119,12 +132,12 @@
|
|||||||
- 新模块
|
- 新模块
|
||||||
- 新页面
|
- 新页面
|
||||||
- 发布动作
|
- 发布动作
|
||||||
- 与账本统一无关的工作
|
- 与检测账本统一无关的工作
|
||||||
|
|
||||||
## 完成下一轮后的预期
|
## 完成下一轮后的预期
|
||||||
|
|
||||||
如果下一轮把账本统一策略明确并落地:
|
如果下一轮把 item 级结果回写规则明确并最小落地:
|
||||||
|
|
||||||
- Detect 页面将不仅“看起来对”
|
- Detect 不仅主统计一致
|
||||||
- 而且底层统计也会真正一致
|
- 而且底层逐条账本也会接近最终一致
|
||||||
- 整体可上线程度预计提升到 `94%~96%`
|
- 整体可上线程度预计提升到 `95%~96%`
|
||||||
|
|||||||
@@ -1,131 +1,138 @@
|
|||||||
# TASK_BOARD
|
# TASK_BOARD
|
||||||
|
|
||||||
更新时间:2026-04-19 02:33 CST
|
更新时间:2026-04-19 02:40 CST
|
||||||
|
|
||||||
## 当前主批次
|
## 当前主批次
|
||||||
|
|
||||||
唯一主批次:`G1-检测总览口径统一批`
|
唯一主批次:`H1-检测账本统一最小落地批`
|
||||||
|
|
||||||
目标:
|
目标:
|
||||||
|
|
||||||
- 不进入新功能实现
|
- 不进入新功能实现
|
||||||
- 不扩展控制面
|
- 不扩展控制面
|
||||||
- 只收口 Detect 页面“大陆明明在跑,但页面像没跑”的口径问题
|
- 只把 Detect 主账本口径从“双轨显示”收敛到“统一后的有效口径”
|
||||||
|
|
||||||
## 本轮已完成
|
## 本轮已完成
|
||||||
|
|
||||||
本轮完成了最小可见修复:
|
本轮完成了最小统一落地:
|
||||||
|
|
||||||
- `detect/job/active`
|
- `detect_job_service.py`
|
||||||
- 新增:
|
- 新增统一账本聚合逻辑:
|
||||||
- `distributed_node_stats`
|
- `_build_effective_node_stats`
|
||||||
- `display_items_claimed`
|
- `_build_effective_summary`
|
||||||
- `display_items_running`
|
- `active_job` 主字段已切换为统一后的有效值:
|
||||||
- `display_items_completed`
|
- `items_pending`
|
||||||
- `display_items_failed`
|
- `items_claimed`
|
||||||
- `display_active_node_codes`
|
- `items_running`
|
||||||
- `detect/queue-summary`
|
- `items_completed`
|
||||||
- 新增:
|
- `items_failed`
|
||||||
- `queue.display_claimed`
|
- `items_terminal`
|
||||||
- `queue.display_running`
|
- `node_stats`
|
||||||
- `nodes` 已切到分布式显示口径
|
- 保留中央原始账本供追溯:
|
||||||
- `DetectView.vue`
|
- `raw_items_total`
|
||||||
- 已优先显示分布式口径
|
- `raw_items_pending`
|
||||||
- 节点表格已优先显示 `distributed_node_stats`
|
- `raw_items_claimed`
|
||||||
|
- `raw_items_running`
|
||||||
|
- `raw_items_completed`
|
||||||
|
- `raw_items_blacklisted`
|
||||||
|
- `raw_items_failed`
|
||||||
|
- `raw_items_terminal`
|
||||||
|
- `raw_node_stats`
|
||||||
|
- 新增单测:
|
||||||
|
- `domain-api/tests/test_detect_job_service.py`
|
||||||
|
|
||||||
## 当前结果快照
|
## 当前结果快照
|
||||||
|
|
||||||
接口验证结果:
|
接口验证结果:
|
||||||
|
|
||||||
- `detect/job/active`
|
- `detect/job/active`
|
||||||
- `display_items_claimed = 34`
|
- 主字段已统一为:
|
||||||
- `display_items_running = 14`
|
- `items_total = 1000`
|
||||||
- `distributed_node_stats` 已包含:
|
- `items_pending = 931`
|
||||||
|
- `items_claimed = 34`
|
||||||
|
- `items_running = 14`
|
||||||
|
- `items_completed = 21`
|
||||||
|
- 原始中央账本仍可追溯:
|
||||||
|
- `raw_items_pending = 950`
|
||||||
|
- `raw_items_claimed = 25`
|
||||||
|
- `raw_items_running = 4`
|
||||||
|
- `node_stats` / `distributed_node_stats` 已统一为:
|
||||||
- `mainland-controller-01`
|
- `mainland-controller-01`
|
||||||
- `mainland-worker-01`
|
- `mainland-worker-01`
|
||||||
- `overseas-control-01`
|
- `overseas-control-01`
|
||||||
- `unassigned`
|
- `unassigned = 680`
|
||||||
- `detect/queue-summary`
|
- `detect/queue-summary`
|
||||||
- `queue.display_claimed = 34`
|
- 主字段已统一为:
|
||||||
- `queue.display_running = 14`
|
- `pending = 931`
|
||||||
- `nodes` 已包含:
|
- `claimed = 34`
|
||||||
- `mainland-controller-01`
|
- `running = 14`
|
||||||
- `mainland-worker-01`
|
|
||||||
- `overseas-control-01`
|
|
||||||
- `unassigned`
|
|
||||||
|
|
||||||
运行侧证据:
|
验证结果:
|
||||||
|
|
||||||
- `mainland-controller-01`
|
- `python3 -m py_compile`
|
||||||
- `runtime.pull_tasks`
|
- 通过
|
||||||
- `job_id = 87`
|
- `/opt/domaincheck/domainCheck/.venv/bin/python -m unittest tests.test_detect_job_service`
|
||||||
- `status = success`
|
- 通过
|
||||||
- 大陆 controller / worker 仍在执行中
|
- `domaincheck-api`
|
||||||
|
- 已重启并验证通过
|
||||||
|
|
||||||
## 当前结论
|
## 当前结论
|
||||||
|
|
||||||
这轮已经把“Detect 页面看不出大陆参与”这个核心观感问题修掉了一大半。
|
这轮已经把“中央账本原值”和“分布式 display 附加值”进一步收敛成一套主展示账本。
|
||||||
|
|
||||||
现在的 Detect 页面口径变成:
|
现在的口径变成:
|
||||||
|
|
||||||
- 中央原始账本仍保留:
|
- 主字段:
|
||||||
- `items_total = 1000`
|
- 直接反映当前统一后的有效检测账本
|
||||||
- `pending = 950`
|
- 原始字段:
|
||||||
- `claimed = 25`
|
- 仍保留中央派发账本,供排障和审计
|
||||||
- `running = 4`
|
|
||||||
- 页面展示口径新增分布式视角:
|
这意味着:
|
||||||
- `display_claimed = 34`
|
|
||||||
- `display_running = 14`
|
- Detect 页面不再只是“看起来对”
|
||||||
- 节点列表能直接看到大陆 controller / worker
|
- Runtime / Queue / Detect 主视图开始吃同一套有效统计
|
||||||
|
|
||||||
## 当前唯一剩余问题
|
## 当前唯一剩余问题
|
||||||
|
|
||||||
唯一剩余问题:
|
唯一剩余问题:
|
||||||
|
|
||||||
- 中央账本与分布式显示口径仍是“双轨”
|
- 这 still 是“聚合统一”,还不是“逐条 item 真正回写统一”
|
||||||
|
|
||||||
这不是 bug,而是当前架构现状:
|
也就是说:
|
||||||
|
|
||||||
- 中央 `detect_job_items` 统计的是海外控制面原始派发账本
|
- 当前已经解决:
|
||||||
- 大陆拉批后的本地执行账本在 runtime / projection 侧
|
- 主统计口径一致
|
||||||
|
- 节点参与显示一致
|
||||||
当前已经做到:
|
- 当前还没解决:
|
||||||
|
- 大陆执行结果逐条回写 `detect_job_items`
|
||||||
- 页面不再误判“大陆没跑”
|
|
||||||
|
|
||||||
但还没做到:
|
|
||||||
|
|
||||||
- 把两套账本真正收敛成一个最终一致的统一任务账本
|
|
||||||
|
|
||||||
## 候选批次
|
## 候选批次
|
||||||
|
|
||||||
### Candidate H1
|
### Candidate I1
|
||||||
|
|
||||||
名称:检测账本统一设计批
|
名称:检测结果逐条回写设计批
|
||||||
|
|
||||||
进入条件:
|
进入条件:
|
||||||
|
|
||||||
- 用户确认下一轮继续深入“中央账本与分布式执行账本统一”
|
- 用户确认要继续把聚合统一推进到 item 级一致
|
||||||
|
|
||||||
范围:
|
范围:
|
||||||
|
|
||||||
- 先设计统一账本规则
|
- 只设计 `detect_result_projection -> detect_job_items` 的最小映射方案
|
||||||
- 不直接大改执行链
|
- 不直接重做整条执行链
|
||||||
- 明确 pull / ack / result sync 后中央账本应如何回写
|
|
||||||
|
|
||||||
### Candidate H2
|
### Candidate I2
|
||||||
|
|
||||||
名称:Detect 页面文案微收口批
|
名称:Detect / Runtime 文案统一批
|
||||||
|
|
||||||
进入条件:
|
进入条件:
|
||||||
|
|
||||||
- 如果用户只想先把页面提示说清楚
|
- 如果用户只想进一步把“raw vs effective”提示写清楚
|
||||||
|
|
||||||
范围:
|
范围:
|
||||||
|
|
||||||
- 在 Detect 页面明确标注:
|
- 补统一文案
|
||||||
- “中央账本视角”
|
- 不再扩逻辑
|
||||||
- “分布式执行视角”
|
|
||||||
|
|
||||||
## 暂停项
|
## 暂停项
|
||||||
|
|
||||||
@@ -141,4 +148,4 @@
|
|||||||
|
|
||||||
下一轮唯一应该继续做的事情:
|
下一轮唯一应该继续做的事情:
|
||||||
|
|
||||||
- 明确中央 `detect_job_items` 与大陆 runtime 投影之间的统一账本策略
|
- 明确是否要继续进入 item 级结果回写统一
|
||||||
|
|||||||
@@ -187,6 +187,96 @@ def _build_display_summary(node_stats: list[dict]) -> dict:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_node_bucket(item: dict) -> dict:
|
||||||
|
node_code = str(item.get("node_code") or "").strip()
|
||||||
|
return {
|
||||||
|
"node_code": node_code,
|
||||||
|
"items_total": _int_value(item.get("items_total")),
|
||||||
|
"items_pending": _int_value(item.get("items_pending")),
|
||||||
|
"items_claimed": _int_value(item.get("items_claimed")),
|
||||||
|
"items_running": _int_value(item.get("items_running")),
|
||||||
|
"items_completed": _int_value(item.get("items_completed")),
|
||||||
|
"items_blacklisted": _int_value(item.get("items_blacklisted")),
|
||||||
|
"items_failed": _int_value(item.get("items_failed")),
|
||||||
|
"metrics_source": str(item.get("metrics_source") or "").strip(),
|
||||||
|
"region": str(item.get("region") or "").strip(),
|
||||||
|
"role": str(item.get("role") or "").strip(),
|
||||||
|
"status": str(item.get("status") or "").strip(),
|
||||||
|
"current_load": _int_value(item.get("current_load")),
|
||||||
|
"last_heartbeat_at": str(item.get("last_heartbeat_at") or "").strip(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_effective_node_stats(
|
||||||
|
*,
|
||||||
|
distributed_node_stats: list[dict],
|
||||||
|
raw_items_total: int,
|
||||||
|
) -> list[dict]:
|
||||||
|
normalized_total = max(0, _int_value(raw_items_total))
|
||||||
|
assigned_buckets: list[dict] = []
|
||||||
|
assigned_total = 0
|
||||||
|
for item in list(distributed_node_stats or []):
|
||||||
|
bucket = _normalize_node_bucket(item)
|
||||||
|
if not bucket["node_code"] or bucket["node_code"] == "unassigned":
|
||||||
|
continue
|
||||||
|
assigned_buckets.append(bucket)
|
||||||
|
assigned_total += bucket["items_total"]
|
||||||
|
|
||||||
|
effective_total = max(normalized_total, assigned_total)
|
||||||
|
remainder_total = max(effective_total - assigned_total, 0)
|
||||||
|
if remainder_total > 0:
|
||||||
|
assigned_buckets.append(
|
||||||
|
{
|
||||||
|
"node_code": "unassigned",
|
||||||
|
"items_total": remainder_total,
|
||||||
|
"items_pending": remainder_total,
|
||||||
|
"items_claimed": 0,
|
||||||
|
"items_running": 0,
|
||||||
|
"items_completed": 0,
|
||||||
|
"items_blacklisted": 0,
|
||||||
|
"items_failed": 0,
|
||||||
|
"metrics_source": "central_queue",
|
||||||
|
"region": "",
|
||||||
|
"role": "",
|
||||||
|
"status": "",
|
||||||
|
"current_load": 0,
|
||||||
|
"last_heartbeat_at": "",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return sorted(
|
||||||
|
assigned_buckets,
|
||||||
|
key=lambda item: (
|
||||||
|
str(item.get("node_code") or "") == "unassigned",
|
||||||
|
-_int_value(item.get("items_running")),
|
||||||
|
-_int_value(item.get("items_claimed")),
|
||||||
|
-_int_value(item.get("items_total")),
|
||||||
|
str(item.get("node_code") or ""),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_effective_summary(*, node_stats: list[dict], raw_items_total: int, raw_items_blacklisted: int) -> dict:
|
||||||
|
effective_total = max(0, _int_value(raw_items_total))
|
||||||
|
effective_blacklisted = max(0, _int_value(raw_items_blacklisted))
|
||||||
|
effective_pending = sum(_int_value(item.get("items_pending")) for item in list(node_stats or []))
|
||||||
|
effective_claimed = sum(_int_value(item.get("items_claimed")) for item in list(node_stats or []))
|
||||||
|
effective_running = sum(_int_value(item.get("items_running")) for item in list(node_stats or []))
|
||||||
|
effective_completed = sum(_int_value(item.get("items_completed")) for item in list(node_stats or []))
|
||||||
|
effective_failed = sum(_int_value(item.get("items_failed")) for item in list(node_stats or []))
|
||||||
|
effective_terminal = effective_completed + effective_blacklisted + effective_failed
|
||||||
|
return {
|
||||||
|
"items_total": effective_total,
|
||||||
|
"items_pending": max(effective_pending, 0),
|
||||||
|
"items_claimed": max(effective_claimed, 0),
|
||||||
|
"items_running": max(effective_running, 0),
|
||||||
|
"items_completed": max(effective_completed, 0),
|
||||||
|
"items_blacklisted": effective_blacklisted,
|
||||||
|
"items_failed": max(effective_failed, 0),
|
||||||
|
"items_terminal": max(effective_terminal, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _extract_current_cycle_events(events: list[dict]) -> tuple[str, list[dict]]:
|
def _extract_current_cycle_events(events: list[dict]) -> tuple[str, list[dict]]:
|
||||||
if not events:
|
if not events:
|
||||||
return "", []
|
return "", []
|
||||||
@@ -293,9 +383,24 @@ def _fetch_job_summary(cur, job_row, event_limit: int = 20) -> dict:
|
|||||||
local_node_stats=list(node_buckets.values()),
|
local_node_stats=list(node_buckets.values()),
|
||||||
runtime_node_rows=_load_runtime_display_rows(cur),
|
runtime_node_rows=_load_runtime_display_rows(cur),
|
||||||
)
|
)
|
||||||
display_summary = _build_display_summary(distributed_node_stats)
|
raw_total = sum(counts.values())
|
||||||
total = sum(counts.values())
|
raw_pending = int(counts.get("pending", 0))
|
||||||
terminal = int(counts.get("completed", 0)) + int(counts.get("blacklisted", 0)) + int(counts.get("failed", 0))
|
raw_claimed = int(counts.get("claimed", 0))
|
||||||
|
raw_running = int(counts.get("running", 0))
|
||||||
|
raw_completed = int(counts.get("completed", 0))
|
||||||
|
raw_blacklisted = int(counts.get("blacklisted", 0))
|
||||||
|
raw_failed = int(counts.get("failed", 0))
|
||||||
|
raw_terminal = raw_completed + raw_blacklisted + raw_failed
|
||||||
|
effective_node_stats = _build_effective_node_stats(
|
||||||
|
distributed_node_stats=distributed_node_stats,
|
||||||
|
raw_items_total=raw_total,
|
||||||
|
)
|
||||||
|
effective_summary = _build_effective_summary(
|
||||||
|
node_stats=effective_node_stats,
|
||||||
|
raw_items_total=raw_total,
|
||||||
|
raw_items_blacklisted=raw_blacklisted,
|
||||||
|
)
|
||||||
|
display_summary = _build_display_summary(effective_node_stats)
|
||||||
return {
|
return {
|
||||||
"job_id": job_id,
|
"job_id": job_id,
|
||||||
"job_code": job_row[1],
|
"job_code": job_row[1],
|
||||||
@@ -305,17 +410,29 @@ def _fetch_job_summary(cur, job_row, event_limit: int = 20) -> dict:
|
|||||||
"created_at": _format_time(job_row[5]),
|
"created_at": _format_time(job_row[5]),
|
||||||
"started_at": _format_time(job_row[6]),
|
"started_at": _format_time(job_row[6]),
|
||||||
"finished_at": _format_time(job_row[7]),
|
"finished_at": _format_time(job_row[7]),
|
||||||
"items_total": total,
|
"items_total": int(effective_summary.get("items_total", 0) or 0),
|
||||||
"items_pending": int(counts.get("pending", 0)),
|
"items_pending": int(effective_summary.get("items_pending", 0) or 0),
|
||||||
"items_claimed": int(counts.get("claimed", 0)),
|
"items_claimed": int(effective_summary.get("items_claimed", 0) or 0),
|
||||||
"items_running": int(counts.get("running", 0)),
|
"items_running": int(effective_summary.get("items_running", 0) or 0),
|
||||||
"items_completed": int(counts.get("completed", 0)),
|
"items_completed": int(effective_summary.get("items_completed", 0) or 0),
|
||||||
"items_blacklisted": int(counts.get("blacklisted", 0)),
|
"items_blacklisted": int(effective_summary.get("items_blacklisted", 0) or 0),
|
||||||
"items_failed": int(counts.get("failed", 0)),
|
"items_failed": int(effective_summary.get("items_failed", 0) or 0),
|
||||||
"items_terminal": terminal,
|
"items_terminal": int(effective_summary.get("items_terminal", 0) or 0),
|
||||||
"progress_percent": round((terminal / total) * 100, 2) if total else 0,
|
"progress_percent": round(
|
||||||
"node_stats": list(node_buckets.values()),
|
(int(effective_summary.get("items_terminal", 0) or 0) / int(effective_summary.get("items_total", 0) or 0)) * 100,
|
||||||
"distributed_node_stats": distributed_node_stats,
|
2,
|
||||||
|
) if int(effective_summary.get("items_total", 0) or 0) else 0,
|
||||||
|
"raw_items_total": raw_total,
|
||||||
|
"raw_items_pending": raw_pending,
|
||||||
|
"raw_items_claimed": raw_claimed,
|
||||||
|
"raw_items_running": raw_running,
|
||||||
|
"raw_items_completed": raw_completed,
|
||||||
|
"raw_items_blacklisted": raw_blacklisted,
|
||||||
|
"raw_items_failed": raw_failed,
|
||||||
|
"raw_items_terminal": raw_terminal,
|
||||||
|
"raw_node_stats": list(node_buckets.values()),
|
||||||
|
"node_stats": effective_node_stats,
|
||||||
|
"distributed_node_stats": effective_node_stats,
|
||||||
"display_items_claimed": int(display_summary.get("items_claimed", 0) or 0),
|
"display_items_claimed": int(display_summary.get("items_claimed", 0) or 0),
|
||||||
"display_items_running": int(display_summary.get("items_running", 0) or 0),
|
"display_items_running": int(display_summary.get("items_running", 0) or 0),
|
||||||
"display_items_completed": int(display_summary.get("items_completed", 0) or 0),
|
"display_items_completed": int(display_summary.get("items_completed", 0) or 0),
|
||||||
|
|||||||
122
domain-api/tests/test_detect_job_service.py
Normal file
122
domain-api/tests/test_detect_job_service.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from app.services.detect_job_service import _build_effective_node_stats, _build_effective_summary
|
||||||
|
|
||||||
|
|
||||||
|
class DetectJobServiceTests(unittest.TestCase):
|
||||||
|
def test_build_effective_node_stats_keeps_runtime_nodes_and_backfills_unassigned(self) -> None:
|
||||||
|
distributed_node_stats = [
|
||||||
|
{
|
||||||
|
"node_code": "mainland-controller-01",
|
||||||
|
"items_total": 200,
|
||||||
|
"items_pending": 195,
|
||||||
|
"items_claimed": 0,
|
||||||
|
"items_running": 5,
|
||||||
|
"items_completed": 0,
|
||||||
|
"items_blacklisted": 0,
|
||||||
|
"items_failed": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node_code": "mainland-worker-01",
|
||||||
|
"items_total": 70,
|
||||||
|
"items_pending": 56,
|
||||||
|
"items_claimed": 9,
|
||||||
|
"items_running": 5,
|
||||||
|
"items_completed": 0,
|
||||||
|
"items_blacklisted": 0,
|
||||||
|
"items_failed": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node_code": "overseas-control-01",
|
||||||
|
"items_total": 50,
|
||||||
|
"items_pending": 0,
|
||||||
|
"items_claimed": 25,
|
||||||
|
"items_running": 4,
|
||||||
|
"items_completed": 21,
|
||||||
|
"items_blacklisted": 0,
|
||||||
|
"items_failed": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node_code": "unassigned",
|
||||||
|
"items_total": 950,
|
||||||
|
"items_pending": 950,
|
||||||
|
"items_claimed": 0,
|
||||||
|
"items_running": 0,
|
||||||
|
"items_completed": 0,
|
||||||
|
"items_blacklisted": 0,
|
||||||
|
"items_failed": 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
node_stats = _build_effective_node_stats(
|
||||||
|
distributed_node_stats=distributed_node_stats,
|
||||||
|
raw_items_total=1000,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(4, len(node_stats))
|
||||||
|
unassigned = next(item for item in node_stats if item["node_code"] == "unassigned")
|
||||||
|
self.assertEqual(680, unassigned["items_total"])
|
||||||
|
self.assertEqual(680, unassigned["items_pending"])
|
||||||
|
self.assertEqual("central_queue", unassigned["metrics_source"])
|
||||||
|
|
||||||
|
def test_build_effective_summary_uses_effective_node_stats_and_raw_blacklisted(self) -> None:
|
||||||
|
node_stats = [
|
||||||
|
{
|
||||||
|
"node_code": "mainland-controller-01",
|
||||||
|
"items_total": 200,
|
||||||
|
"items_pending": 195,
|
||||||
|
"items_claimed": 0,
|
||||||
|
"items_running": 5,
|
||||||
|
"items_completed": 0,
|
||||||
|
"items_blacklisted": 0,
|
||||||
|
"items_failed": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node_code": "mainland-worker-01",
|
||||||
|
"items_total": 70,
|
||||||
|
"items_pending": 56,
|
||||||
|
"items_claimed": 9,
|
||||||
|
"items_running": 5,
|
||||||
|
"items_completed": 0,
|
||||||
|
"items_blacklisted": 0,
|
||||||
|
"items_failed": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node_code": "overseas-control-01",
|
||||||
|
"items_total": 50,
|
||||||
|
"items_pending": 0,
|
||||||
|
"items_claimed": 25,
|
||||||
|
"items_running": 4,
|
||||||
|
"items_completed": 21,
|
||||||
|
"items_blacklisted": 0,
|
||||||
|
"items_failed": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"node_code": "unassigned",
|
||||||
|
"items_total": 680,
|
||||||
|
"items_pending": 680,
|
||||||
|
"items_claimed": 0,
|
||||||
|
"items_running": 0,
|
||||||
|
"items_completed": 0,
|
||||||
|
"items_blacklisted": 0,
|
||||||
|
"items_failed": 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
summary = _build_effective_summary(
|
||||||
|
node_stats=node_stats,
|
||||||
|
raw_items_total=1000,
|
||||||
|
raw_items_blacklisted=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(1000, summary["items_total"])
|
||||||
|
self.assertEqual(931, summary["items_pending"])
|
||||||
|
self.assertEqual(34, summary["items_claimed"])
|
||||||
|
self.assertEqual(14, summary["items_running"])
|
||||||
|
self.assertEqual(21, summary["items_completed"])
|
||||||
|
self.assertEqual(3, summary["items_blacklisted"])
|
||||||
|
self.assertEqual(24, summary["items_terminal"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user