diff --git a/.wolf/buglog.json b/.wolf/buglog.json index 8e0357de..ec54f4b7 100644 --- a/.wolf/buglog.json +++ b/.wolf/buglog.json @@ -14797,5 +14797,24 @@ ], "occurrences": 1, "last_seen": "2026-08-05" + }, + { + "id": "bug-1504", + "timestamp": "2026-08-05", + "error_message": "xcecli jobs list -a \u8fd4\u56de \"no job found\"\uff08\u5e94\u663e\u793a\u5168\u90e8\u4f5c\u4e1a\u542b\u5df2\u5b8c\u6210\uff09\uff1b\u9ed8\u8ba4\u5217\u8868\u53ea\u663e\u793a RUN \u4f5c\u4e1a", + "file": "src/frontend/apiserver/store/query.go (MatchesBitmaskCondition) + query_test.go", + "root_cause": "FilterRows \u628a bitmask \u5b57\u6bb5 (job_status/host_status) \u7684\u6240\u6709\u6761\u4ef6\u90fd\u8def\u7531\u5230 MatchesBitmaskCondition\uff0c\u4f46\u8be5\u51fd\u6570\u53ea\u5b9e\u73b0 OpEQ/OpNE \u7684\u4f4d\u63a9\u7801\u8bed\u4e49\u3002 \u751f\u6210\u7684 (OpNOTNULL) \u843d\u5165 -> \u6240\u6709\u4f5c\u4e1a\u88ab\u8fc7\u6ee4\u3002\u9ed8\u8ba4\u5217\u8868\u7684 notin(EXIT,DONE) \u88ab\u7f16\u8bd1\u6210 OpEQ(mask) \u6240\u4ee5\u4e0d\u53d7\u5f71\u54cd\u3002", + "fix": "MatchesBitmaskCondition \u589e\u52a0 OpISNULL/OpNOTNULL \u5206\u652f\uff0c\u76f4\u63a5\u5728\u539f\u59cb\u5b57\u6bb5\u503c\u4e0a\u5224\u7a7a\u3002\u65b0\u589e query_test.go 3 \u4e2a\u56de\u5f52\u6d4b\u8bd5\uff08notnull \u5168\u5339\u914d / null \u7a7a\u5339\u914d / notin \u4f4d\u63a9\u7801\u8def\u5f84\u4e0d\u56de\u5f52\uff09\uff0cRED->GREEN \u9a8c\u8bc1\u3002", + "tags": [ + "apiserver", + "xcecli", + "jobs-list", + "filter", + "bitmask", + "notnull" + ], + "related_bugs": [], + "occurrences": 1, + "last_seen": "2026-08-05" } ] \ No newline at end of file diff --git a/.wolf/memory.md b/.wolf/memory.md index 5868d988..5867910a 100644 --- a/.wolf/memory.md +++ b/.wolf/memory.md @@ -7370,3 +7370,4 @@ | 05:50 | Session end: 9 writes across 5 files (frpcdbg_mon.sh, frpc_prod.toml, frpc_mon.sh, frpc_watchdog.sh, frpc-watchdog.sh) | 2 reads | ~2219 tok | | 09:39 | 修复 controller 反复崩溃 bug-1502: persist_job_hook_fun 把 RUSAGE_UPDATE hook 的第2参 (xce_job_rusages_t*) 当 job_t* 用, 读 offset 56 越界 56字节结构 -> ASAN heap-buffer-overflow. 加专用 case 从 jobmgr 取当前 state | src/controller/persist.c, testsuite/test_persist_hooks.c | RED(旧代码 stack-buffer-overflow)/GREEN(修复后 PASS), 全套458测试通过 | ~350 | | 01:08 | 验证完成: bug-1502(controller ASAN崩溃)修复生产生效(sleep 666跑满666秒, controller 0崩溃); 作业PENDING根因=配置正确但scheduler先于worker join拿不到slots(操作时序, bug-1503); 干净controller部署, 集群全链路(apiserver提交->scheduler->controller->worker)冒烟通过 | persist.c, sched.c, worker.c | 验证通过 | ~300 | +| 01:42 | 修复 bug-1504: xcecli jobs list -a 返回空, 因 FilterRows 把 job_status notnull 路由到 MatchesBitmaskCondition(仅实现OpEQ/OpNE) -> 全过滤. 加 OpISNULL/OpNOTNULL 分支 + 3回归测试, 部署后 -a 正常显示全部作业 | src/frontend/apiserver/store/query.go + query_test.go | RED->GREEN, 生产验证 -a 列出全部 | ~250 | diff --git a/src/frontend/apiserver/store/query.go b/src/frontend/apiserver/store/query.go index 508dbc45..631852e4 100644 --- a/src/frontend/apiserver/store/query.go +++ b/src/frontend/apiserver/store/query.go @@ -78,8 +78,18 @@ func MatchesCondition(row utils.OrderMap, cond utils.Condition) bool { // MatchesBitmaskCondition handles bitmask fields (job_status, host_status). // For bitmask EQ: (val & mask) != 0 means the status is set. // For bitmask NE: (val & mask) == 0 means the status is NOT set. +// null/notnull are handled on the raw field value (bug-1504: job_status +// notnull previously fell through to `return false`, so `jobs list -a` +// filtered out every job). func MatchesBitmaskCondition(row utils.OrderMap, field string, bitmask int64, op utils.OperatorType) bool { - val := toInt64(row.Data[field]) + fieldVal := row.Data[field] + switch op { + case utils.OpISNULL: + return fieldVal == nil + case utils.OpNOTNULL: + return fieldVal != nil + } + val := toInt64(fieldVal) result := val & bitmask switch op { case utils.OpNE: // bitmask "eq" is stored as OpNE (val & mask != 0) diff --git a/src/frontend/apiserver/store/query_test.go b/src/frontend/apiserver/store/query_test.go new file mode 100644 index 00000000..a1d1f9b6 --- /dev/null +++ b/src/frontend/apiserver/store/query_test.go @@ -0,0 +1,85 @@ +package store + +import ( + "testing" + + "apiserver/utils" +) + +// makeJobRow builds a row in the OrderMap shape QueryData produces for a job. +func makeJobRow(jobid int64, status int) utils.OrderMap { + return utils.OrderMap{ + Fields: []string{"job_id", "job_status", "submit_user", "queue_name"}, + Data: map[string]interface{}{ + "job_id": jobid, + "job_status": status, + "submit_user": "admin", + "queue_name": "normal", + }, + } +} + +// bug-1504: `xcecli jobs list -a` (job_status notnull) filtered out EVERY job. +// job_status is a bitmask field, so FilterRows routed the OpNOTNULL condition +// to MatchesBitmaskCondition, which only implements OpEQ/OpNE bitmask +// semantics and fell through to `return false` — the "all jobs" query always +// returned "no job found". +func TestFilterRows_JobStatusNotNull_MatchesAllJobs(t *testing.T) { + rows := []utils.OrderMap{ + makeJobRow(1, 0x02), // RUN + makeJobRow(2, 0x04), // EXIT + makeJobRow(3, 0x08), // DONE + } + + cond := utils.Condition{ + Column: "job_status", + Operator: utils.OpNOTNULL, + Value: utils.Value{Type: utils.TypeInt, Value: int64(0)}, + } + + got := FilterRows(rows, []utils.Condition{cond}) + if len(got) != len(rows) { + t.Fatalf("job_status notnull: got %d rows, want %d (all jobs have a status)", len(got), len(rows)) + } +} + +func TestFilterRows_JobStatusIsNull_MatchesNothing(t *testing.T) { + rows := []utils.OrderMap{ + makeJobRow(1, 0x02), + makeJobRow(2, 0x04), + } + + cond := utils.Condition{ + Column: "job_status", + Operator: utils.OpISNULL, + Value: utils.Value{Type: utils.TypeInt, Value: int64(0)}, + } + + got := FilterRows(rows, []utils.Condition{cond}) + if len(got) != 0 { + t.Fatalf("job_status null: got %d rows, want 0", len(got)) + } +} + +// Regression guard: the default list filter (job_status notin EXIT/DONE → +// bitmask OpEQ with mask 0x0C) must keep working after the null/notnull fix. +func TestFilterRows_JobStatusNotinExitDone(t *testing.T) { + rows := []utils.OrderMap{ + makeJobRow(1, 0x02), // RUN — should survive + makeJobRow(2, 0x04), // EXIT — filtered + makeJobRow(3, 0x0C), // EXIT|DONE — filtered + } + + // notin (EXIT,DONE) on a bitmask field is compiled to OpEQ with mask + // EXIT|DONE = 0x0C: keep rows where (val & mask) == 0. + cond := utils.Condition{ + Column: "job_status", + Operator: utils.OpEQ, + Value: utils.Value{Type: utils.TypeInt, Value: int64(0x0C)}, + } + + got := FilterRows(rows, []utils.Condition{cond}) + if len(got) != 1 || got[0].Data["job_status"] != 0x02 { + t.Fatalf("job_status notin(EXIT,DONE): got %d rows, want exactly the RUN job", len(got)) + } +}