Skip to content

Commit f97677a

Browse files
author
kylincaster
committed
fix: timeout/PAUSE job lifecycle — CPU binding, cgroup cleanup, and block -r on PAUSE
Four interrelated fixes for PAUSE (timeout) job handling: 1. Block 'ts -r' on PAUSE jobs (like RUNNING) - s_remove_job() now rejects PAUSE state the same as RUNNING - Avoids cgroups_clean_job() on live processes 2. Free CPU binding on PAUSE (safe_pause_job) - cpu_bind_free() when pausing, so allocator can reassign CPUs - Re-allocate on PAUSE→RUNNING resume in config_running() 3. Fix double CPU allocation for QUEUED→RUNNING - Guard re-allocation in config_running() with p->pid != 0 - QUEUED jobs get allocated later in s_process_runjob_ok() 4. Fix cgroup cleanup order for v1 - Swap cleanup_freeze after cleanup_cpu in cgroups_clean_job() - CPU cleanup kills processes first, freeze rmdir succeeds immediately Also: cgroups v1/v2 init validation, reconnect improvements, start-service flag, non-fatal cpu.cfs_quota_us warnings.
1 parent 06d27fb commit f97677a

20 files changed

Lines changed: 1452 additions & 28 deletions

.claude/remote-trigger-audit.jsonl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{"auditId":"a8e5dadf-b5d2-4cc2-90a2-08692b83bf6e","ok":false,"error":"Not authenticated with a claude.ai account. Run /login and try again.","createdAt":1780452897448}
2+
{"auditId":"13526caa-c23f-4d07-95d4-daebf154d6bc","ok":false,"error":"Not authenticated with a claude.ai account. Run /login and try again.","createdAt":1780452903533}
3+
{"auditId":"cae3aff9-3566-42ab-be48-5d4c4306e040","ok":false,"error":"Not authenticated with a claude.ai account. Run /login and try again.","createdAt":1780452925545}
4+
{"auditId":"77586bc1-5c0e-484d-87f5-1182ba975879","ok":false,"error":"Not authenticated with a claude.ai account. Run /login and try again.","createdAt":1780452943937}
5+
{"auditId":"cddb29a3-41e0-440d-aedf-26f9a4e9f407","ok":false,"error":"Not authenticated with a claude.ai account. Run /login and try again.","createdAt":1780452954295}
6+
{"auditId":"331a804d-cb4c-4304-b637-79a09ce19494","ok":false,"error":"Not authenticated with a claude.ai account. Run /login and try again.","createdAt":1780452971227}
7+
{"auditId":"deb5a7b1-db62-4185-a5c5-00f7a895fbed","ok":false,"error":"Not authenticated with a claude.ai account. Run /login and try again.","createdAt":1780452976216}
8+
{"auditId":"f21b7b91-e3f7-4813-9f73-a63aa070dddd","ok":false,"error":"Not authenticated with a claude.ai account. Run /login and try again.","createdAt":1780453012872}

.codegraph/.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# CodeGraph data files
2+
# These are local to each machine and should not be committed
3+
4+
# Database
5+
*.db
6+
*.db-wal
7+
*.db-shm
8+
9+
# Cache
10+
cache/
11+
12+
# Logs
13+
*.log
14+
15+
# Hook markers
16+
.dirty

cgroups.c

Lines changed: 108 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,14 @@ static inline void cg_group_name(int jobid, pid_t pid, char *buf, size_t size) {
9393
void cgroups_v2_init(void) {
9494
FILE *fp = fopen(CGROUP_CONTROLLERS, "r");
9595
if (!fp) {
96-
fprintf(stderr, "Warning: cannot open %s\n", CGROUP_CONTROLLERS);
97-
return;
96+
error("Cannot open %s — cgroup v2 not available", CGROUP_CONTROLLERS);
9897
}
9998

10099
char line[256];
101100
int has_cpu = 0;
101+
#ifdef TS_CPU_BIND
102102
int has_cpuset = 0;
103+
#endif
103104
if (fgets(line, sizeof(line), fp)) {
104105
if (strstr(line, "cpu")) {
105106
has_cpu = 1;
@@ -112,19 +113,65 @@ void cgroups_v2_init(void) {
112113
}
113114
fclose(fp);
114115

115-
if (has_cpu) {
116-
cg_write(CGROUP_SUBTREE_CONTROL, "+cpu");
116+
/* cpu controller is required */
117+
if (!has_cpu) {
118+
error("cgroup v2: 'cpu' controller not found in %s. "
119+
"The kernel or container does not support it.",
120+
CGROUP_CONTROLLERS);
121+
}
122+
123+
/* Write +cpu and verify */
124+
if (cg_write(CGROUP_SUBTREE_CONTROL, "+cpu") != 0) {
125+
error("cgroup v2: failed to enable 'cpu' controller — "
126+
"check %s permissions", CGROUP_SUBTREE_CONTROL);
117127
}
128+
{
129+
char verify[256] = "";
130+
FILE *vfp = fopen(CGROUP_SUBTREE_CONTROL, "r");
131+
if (vfp) {
132+
if (fgets(verify, sizeof(verify), vfp))
133+
verify[strcspn(verify, "\n")] = '\0';
134+
fclose(vfp);
135+
}
136+
if (!strstr(verify, "cpu")) {
137+
error("cgroup v2: 'cpu' controller not propagated to %s "
138+
"(got \"%s\"). Check parent cgroup subtree_control.",
139+
CGROUP_SUBTREE_CONTROL, verify);
140+
}
141+
}
142+
118143
#ifdef TS_CPU_BIND
119-
if (has_cpuset) {
120-
cg_write(CGROUP_SUBTREE_CONTROL, "+cpuset");
144+
if (!has_cpuset) {
145+
error("cgroup v2: 'cpuset' controller not found in %s. "
146+
"CPU binding requires it.",
147+
CGROUP_CONTROLLERS);
148+
}
149+
150+
if (cg_write(CGROUP_SUBTREE_CONTROL, "+cpuset") != 0) {
151+
error("cgroup v2: failed to enable 'cpuset' controller — "
152+
"check %s permissions", CGROUP_SUBTREE_CONTROL);
153+
}
154+
{
155+
char verify[256] = "";
156+
FILE *vfp = fopen(CGROUP_SUBTREE_CONTROL, "r");
157+
if (vfp) {
158+
if (fgets(verify, sizeof(verify), vfp))
159+
verify[strcspn(verify, "\n")] = '\0';
160+
fclose(vfp);
161+
}
162+
if (!strstr(verify, "cpuset")) {
163+
error("cgroup v2: 'cpuset' controller not propagated to %s "
164+
"(got \"%s\"). Check parent cgroup subtree_control.",
165+
CGROUP_SUBTREE_CONTROL, verify);
166+
}
121167
}
122168
#endif
123169
}
124170

125171
static int cgroups_v2_cpu(int jobid, pid_t pid, int cpus) {
126172
char path[512];
127173
char group[64];
174+
int err = 0;
128175

129176
cg_group_name(jobid, pid, group, sizeof(group));
130177
snprintf(path, sizeof(path), CGROUP_DIR "/%s", group);
@@ -139,15 +186,17 @@ static int cgroups_v2_cpu(int jobid, pid_t pid, int cpus) {
139186

140187
snprintf(path, sizeof(path), CGROUP_DIR "/%s/" CGROUP_CPU_MAX, group);
141188
if (cg_write(path, "%ld %ld", quota, period) != 0) {
142-
return -1;
189+
fprintf(stderr, "Warning: cannot set cpu.max for job %d — "
190+
"running without CPU limit\n", jobid);
191+
err = 1;
143192
}
144193

145194
snprintf(path, sizeof(path), CGROUP_DIR "/%s/" CGROUP_PROCS, group);
146195
if (cg_write(path, "%d", pid) != 0) {
147196
return -1;
148197
}
149198

150-
return 0;
199+
return err ? -1 : 0;
151200
}
152201

153202
static int cgroups_v2_wait_frozen(int jobid, pid_t pid) {
@@ -366,9 +415,47 @@ static int cgroups_v2_freeze_ok(int jobid, pid_t pid) {
366415

367416
/* ---- v1 internal ---- */
368417

418+
void cgroups_v1_init(void) {
419+
struct stat st;
420+
421+
/* cpu controller: must exist and be writable */
422+
if (stat(CGROUP_V1_CPU_DIR, &st) != 0 || !S_ISDIR(st.st_mode)) {
423+
error("cgroup v1: '%s' not found — kernel or container "
424+
"does not support cpu controller", CGROUP_V1_CPU_DIR);
425+
}
426+
if (access(CGROUP_V1_CPU_DIR, W_OK) != 0) {
427+
error("cgroup v1: '%s' not writable — run as root",
428+
CGROUP_V1_CPU_DIR);
429+
}
430+
431+
/* freezer controller: must exist and be writable */
432+
if (stat(CGROUP_V1_FREEZE_DIR, &st) != 0 || !S_ISDIR(st.st_mode)) {
433+
error("cgroup v1: '%s' not found — kernel or container "
434+
"does not support freezer controller", CGROUP_V1_FREEZE_DIR);
435+
}
436+
if (access(CGROUP_V1_FREEZE_DIR, W_OK) != 0) {
437+
error("cgroup v1: '%s' not writable — run as root",
438+
CGROUP_V1_FREEZE_DIR);
439+
}
440+
441+
#ifdef TS_CPU_BIND
442+
/* cpuset controller: required for CPU binding */
443+
if (stat("/sys/fs/cgroup/cpuset", &st) != 0 || !S_ISDIR(st.st_mode)) {
444+
error("cgroup v1: '/sys/fs/cgroup/cpuset' not found — "
445+
"CPU binding requires cpuset controller");
446+
}
447+
if (access("/sys/fs/cgroup/cpuset", W_OK) != 0) {
448+
error("cgroup v1: '/sys/fs/cgroup/cpuset' not writable — "
449+
"run as root");
450+
}
451+
#endif
452+
}
453+
369454
static int cgroups_v1_cpu(int jobid, pid_t pid, int cpus) {
370455
char buf[256];
371456
char group[64];
457+
int err = 0;
458+
372459
cg_group_name(jobid, pid, group, sizeof(group));
373460
snprintf(buf, sizeof(buf), CGROUP_V1_CPU_DIR "/%s", group);
374461
printf("Create cgroups folder at %s\n", buf);
@@ -383,19 +470,23 @@ static int cgroups_v1_cpu(int jobid, pid_t pid, int cpus) {
383470

384471
snprintf(path, sizeof(path), "%s/cpu.cfs_period_us", buf);
385472
if (cg_write(path, "%ld", period) != 0) {
386-
return -1;
473+
fprintf(stderr, "Warning: cannot set cpu.cfs_period_us for job %d — "
474+
"running without CPU limit\n", jobid);
475+
err = 1;
387476
}
388477

389478
snprintf(path, sizeof(path), "%s/cpu.cfs_quota_us", buf);
390479
if (cg_write(path, "%ld", quota) != 0) {
391-
return -1;
480+
fprintf(stderr, "Warning: cannot set cpu.cfs_quota_us for job %d — "
481+
"running without CPU limit\n", jobid);
482+
err = 1;
392483
}
393484

394485
snprintf(path, sizeof(path), "%s/cgroup.procs", buf);
395486
if (cg_write(path, "%d", pid) != 0) {
396487
return -1;
397488
}
398-
return 0;
489+
return err ? -1 : 0;
399490
}
400491

401492
static int cgroups_v1_freeze(int jobid, pid_t pid) {
@@ -721,8 +812,8 @@ void cgroups_clean_job(const struct Job *p) {
721812
cgroups_v2_cleanup(group);
722813
#else
723814
printf("clear cgroups: %s\n", group);
724-
cgroups_v1_cleanup_freeze(group);
725815
cgroups_v1_cleanup_cpu(group);
816+
cgroups_v1_cleanup_freeze(group);
726817
#ifdef TS_CPU_BIND
727818
cgroups_v1_cleanup_cpuset(group);
728819
#endif
@@ -757,6 +848,11 @@ void cgroups_set_cpuset(int jobid, pid_t pid, const void *valloc)
757848
cgroup_io_wait_if_busy();
758849

759850
const struct CpuAlloc *alloc = (const struct CpuAlloc *)valloc;
851+
852+
/* 防御:分配失败时不写 cpuset(避免写空 cpuset.cpus → cgroup.procs ENOSPC) */
853+
if (!alloc || alloc->error || alloc->count <= 0)
854+
return;
855+
760856
char *cpus_str = cpu_bind_format_cpus(alloc);
761857
char *mems_str = cpu_bind_format_mems(alloc);
762858
char path[512];

cgroups.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ int cgroups_freeze_ok(int jobid, pid_t pid);
2222
#ifdef CGROUP_V2
2323
void cgroups_v2_init(void);
2424
#endif
25+
void cgroups_v1_init(void);
2526

2627
#ifdef TS_CPU_BIND
2728
void cgroups_set_cpuset(int jobid, pid_t pid, const void *alloc);

cpu_bind/HANDOVER.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# CPU 绑定分配器 — 交接文档
2+
3+
## 文件结构
4+
5+
```
6+
cpu_bind/
7+
├── topology.h # 硬编码系统拓扑(2节点 x 4组 x 4核 = 32核)
8+
├── cpu_bind.h # 公共 API 声明
9+
├── cpu_bind.c # 完整实现
10+
├── test_cpu_bind.c # 16 个测试用例
11+
└── Makefile # 独立编译(依赖 ../vec.h ../vec.o)
12+
```
13+
14+
## 公共 API
15+
16+
```c
17+
void cpu_bind_init(void); // 初始化
18+
int cpu_bind_enabled(void); // 是否启用
19+
struct CpuAlloc *cpu_bind_alloc_init(int jobid, int max_cpus); // 创建空 alloc
20+
void cpu_bind_alloc(struct CpuAlloc *alloc, int N); // 分配 N 个 CPU
21+
void cpu_bind_free(struct CpuAlloc *alloc); // 释放
22+
int cpu_bind_defrag(void); // 碎片整理
23+
char *cpu_bind_format_cpus(const struct CpuAlloc *alloc); // "0,1,2,3"
24+
char *cpu_bind_format_mems(const struct CpuAlloc *alloc); // "0" 或 "0,1"
25+
```
26+
27+
## struct CpuAlloc
28+
29+
```c
30+
struct CpuAlloc {
31+
int primary_node; // 主节点(-1=不限制)
32+
int mem_nodes; // 涉及节点位图
33+
int count; // 已分配的 CPU 数
34+
int slots; // 容量(os_cpus[] 最大元素数)
35+
int jobid;
36+
int error; // 0=成功 1=异常
37+
int quality; // 碎片 group 数(0=完美 1=1个碎片 2=2个碎片)
38+
int os_cpus[]; // 柔性数组
39+
};
40+
```
41+
42+
## 全局变量
43+
44+
- `cpu_owner[MAX_OS_CPU]` — CPU→jobid 映射(0=空闲)
45+
- `cpu_to_group[MAX_OS_CPU]` — CPU→group 索引表(`cpu_bind_init` 中构建)
46+
- `cpu_bind_disabled``TS_NO_CPU_BIND` 环境变量禁用
47+
- `cpu_allocs``vec_t` 全局活跃 alloc 列表(供 defrag 使用)
48+
49+
## 内部函数调用链
50+
51+
```
52+
cpu_bind_alloc()
53+
├─ single_ok? → alloc_from_groups(N, node->group_ids, ...)
54+
└─ 否则 → cross_node_merge()
55+
└─ alloc_from_groups(N, collected_ids, ...)
56+
57+
alloc_from_groups(N, group_ids, ...)
58+
while remaining > 0:
59+
① best-fit 单组(free_count ≥ remaining,excess 最小)
60+
② 无 → 最大空闲组
61+
└─ take_from_group(group, take, alloc)
62+
63+
take_from_group()
64+
→ 在 group→os_cpus[] 中取 N 个空闲 CPU(不要求连续)
65+
→ 写 cpu_owner,更新 free_count
66+
→ free_count > 0 则 alloc->quality++
67+
→ 返回实际取的个数
68+
69+
cpu_bind_post_alloc()
70+
→ qsort(os_cpus)
71+
→ 校验①:每个 CPU owner == jobid
72+
→ 校验②:owner 总数 == count
73+
→ 可 #define CPU_BIND_NO_POST_CHECK 关闭
74+
```
75+
76+
## defrag 流程
77+
78+
```
79+
cpu_bind_defrag()
80+
① 按 (quality ASC, slots DESC) 排序 cpu_allocs
81+
② 找 quality=0 的边界
82+
③ 清空 cpu_owner,重置 free_count(用 num_cores 直接赋值)
83+
④ 恢复 quality=0 allocs 的 cpu_owner
84+
⑤ 重分 quality>0 allocs(手动重置字段,re-alloc)
85+
```
86+
87+
## 测试
88+
89+
```bash
90+
cd cpu_bind && make test
91+
# 16/16 passed, 零警告
92+
```
93+
94+
`test_cpu_bind.c` — 16 个测试覆盖:初始化、禁用、空/超容量分配、单核→整组→跨组→跨节点、耗尽+重分配、双释放安全、格式化、并发不重叠、defrag。
95+
96+
## 待办 / 后续
97+
98+
- [ ] defrag 的质量排序和重分逻辑需要实际验证(目前只有空 vec 测试)
99+
- [ ] `cpu_bind_free` 中的 vec 扫描删除是 O(n),数据量大时可优化
100+
- [ ] `cpu_bind_post_alloc` 的校验目前硬编码,可通过编译开关关闭
101+
- [ ] 与 ts 主项目的集成(`cpu_bind.c` 需要接入 server 的 job 生命周期)

cpu_bind/Makefile

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# cpu_bind/Makefile — 独立 CPU 绑定模块编译 & 测试
2+
3+
CC = gcc
4+
CFLAGS = -std=c11 -Wall -Wextra -g -D_DEFAULT_SOURCE -I..
5+
6+
.PHONY: all test clean
7+
8+
all: test_cpu_bind
9+
10+
cpu_bind.o: cpu_bind.c cpu_bind.h topology.h ../vec.h
11+
$(CC) $(CFLAGS) -c cpu_bind.c -o cpu_bind.o
12+
13+
test_cpu_bind: test_cpu_bind.c cpu_bind.o cpu_bind.h topology.h ../vec.h
14+
$(CC) $(CFLAGS) -o test_cpu_bind test_cpu_bind.c cpu_bind.o ../vec.o
15+
16+
test: test_cpu_bind
17+
./test_cpu_bind
18+
19+
clean:
20+
rm -f *.o test_cpu_bind

0 commit comments

Comments
 (0)