Skip to content

Commit 61b28ba

Browse files
author
kylincaster
committed
feat: add health check for RUNNING jobs
- New ABNORMAL state in enum Jobstate — set when health check detects a RUNNING job is stuck (output empty AND no child processes) - s_check_running_health() — periodic check, runs every 10s in server loop - count_child_pids() — reads /proc/PID/children to detect zombie jobs - health_state field in struct Job — 0=UNCHECKED, 1=NORMAL, skip on rechecks - On ABNORMAL: free slots via free_cores(), release CPU binding, state changes to ABNORMAL so it's excluded from slot accounting - s_remove_job() rejects removing ABNORMAL jobs (use ts -k instead) - s_send_output() allows ABNORMAL state so ts -k can get the PID - Only adds new code — zero changes to existing logic paths
1 parent f97677a commit 61b28ba

4 files changed

Lines changed: 129 additions & 1 deletion

File tree

job_ops.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -505,7 +505,7 @@ void s_send_output(int s, int jobid) {
505505
} else {
506506
p = get_job(jobid);
507507
if (p != 0 && p->state != RUNNING && p->state != FINISHED && p->state != SKIPPED
508-
&& p->state != PAUSE)
508+
&& p->state != PAUSE && p->state != ABNORMAL)
509509
p = 0;
510510
/* Thaw PAUSEd jobs so signals can be delivered */
511511
if (p != 0 && p->state == PAUSE) {

jobs.c

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,79 @@ static int s_check_timeout() {
252252
return had_timeout;
253253
}
254254

255+
/* Forward declaration */
256+
static int count_child_pids(pid_t parent_pid);
257+
258+
/* Health check for RUNNING jobs.
259+
Checks jobs that have been RUNNING for ≥300 seconds and are HEALTH_UNCHECKED.
260+
Conditions indicating an unhealthy job:
261+
1) output log file exists but is empty (stat size == 0)
262+
2) no child processes (only the shell waiting)
263+
Fulfilling any condition → HEALTH_ABNORMAL, free cores & CPU binding.
264+
Otherwise → HEALTH_NORMAL (skip on future checks).
265+
Does NOT freeze cgroup — ts -k must keep working as usual. */
266+
void s_check_running_health(void) {
267+
size_t n = vec_size(&active_jobs);
268+
for (size_t i = 0; i < n; i++) {
269+
struct Job *p = (struct Job *)vec_get(&active_jobs, i);
270+
271+
/* Only check RUNNING, UNCHECKED jobs with a real PID */
272+
if (p->state != RUNNING) continue;
273+
if (p->health_state != 0) continue; /* 0 = HEALTH_UNCHECKED */
274+
if (p->pid <= 0) continue;
275+
276+
/* Must have been running for at least 5 minutes (300 seconds) */
277+
time_t elapsed = get_monotonic_sec() - p->info.start_time;
278+
if (elapsed < 300) continue;
279+
280+
int abnormal = 0;
281+
282+
/* Conditions: output file empty AND no child processes — both required */
283+
if (!abnormal) {
284+
int output_empty = 0;
285+
int no_children = 0;
286+
287+
if (p->output_filename != NULL && p->output_filename[0] != '\0') {
288+
struct stat st;
289+
if (stat(p->output_filename, &st) == 0 && S_ISREG(st.st_mode) && st.st_size == 0) {
290+
output_empty = 1;
291+
}
292+
}
293+
294+
if (count_child_pids(p->pid) == 0) {
295+
no_children = 1;
296+
}
297+
298+
if (output_empty && no_children) {
299+
printf("Job[%d] health ABNORMAL: output empty + no child processes (pid=%d)\n",
300+
p->jobid, p->pid);
301+
abnormal = 1;
302+
}
303+
}
304+
305+
if (abnormal) {
306+
p->state = ABNORMAL;
307+
/* Free slots so new jobs can dispatch */
308+
if (p->num_allocated != 0) {
309+
free_cores(p);
310+
}
311+
#ifdef TS_CPU_BIND
312+
/* Free CPU binding if allocated */
313+
if (p->cpu_alloc) {
314+
cpu_bind_free((struct CpuAlloc *)p->cpu_alloc);
315+
p->cpu_alloc = NULL;
316+
}
317+
if (cpu_bind_defrag_enabled())
318+
cpu_bind_defrag_start();
319+
#endif
320+
printf("Job[%d] -> ABNORMAL (slots freed)\n", p->jobid);
321+
} else {
322+
p->health_state = 1; /* HEALTH_NORMAL */
323+
printf("Job[%d] -> HEALTH_NORMAL\n", p->jobid);
324+
}
325+
}
326+
}
327+
255328
int s_update_slots_usage() {
256329
int timeout_flag = s_check_timeout();
257330

@@ -318,6 +391,37 @@ static int is_descendant_pid(pid_t parent_pid, pid_t target_pid) {
318391
return 0;
319392
}
320393

394+
/* Count direct child PIDs of a process by reading /proc/<pid>/children.
395+
Returns the number of children, 0 if none or error. */
396+
static int count_child_pids(pid_t parent_pid) {
397+
char path[256];
398+
char buf[4096];
399+
int fd;
400+
401+
snprintf(path, sizeof(path), "/proc/%d/task/%d/children", parent_pid, parent_pid);
402+
fd = open(path, O_RDONLY);
403+
if (fd == -1) {
404+
return 0;
405+
}
406+
407+
ssize_t n = read(fd, buf, sizeof(buf) - 1);
408+
close(fd);
409+
410+
if (n <= 0) {
411+
return 0;
412+
}
413+
buf[n] = '\0';
414+
415+
int count = 0;
416+
char *saveptr;
417+
char *token = strtok_r(buf, " ", &saveptr);
418+
while (token != NULL) {
419+
count++;
420+
token = strtok_r(NULL, " ", &saveptr);
421+
}
422+
return count;
423+
}
424+
321425
/* Find which running job a PID belongs to.
322426
If deep_search != 0, also checks child/grandchild processes.
323427
Returns jobid if found, -1 if not. */
@@ -524,6 +628,7 @@ char const *jstate2string(enum Jobstate s) {
524628
case WAIT: jobstate = "wait "; break;
525629
case DELINK: jobstate = "delink "; break;
526630
case LOCKED: jobstate = "locked "; break;
631+
case ABNORMAL: jobstate = "abnormal"; break;
527632
case PAUSE: jobstate = "pause "; break;
528633
default: jobstate = "UNKNOWN ";
529634
}
@@ -1284,6 +1389,16 @@ int s_remove_job(int s, int *jobid, struct User *client) {
12841389
return 0;
12851390
}
12861391

1392+
if (p->state == ABNORMAL) {
1393+
if (*jobid == -1)
1394+
snprintf(buff, 255, "Last job is abnormal.\n");
1395+
else
1396+
snprintf(buff, 255, "Abnormal job [%i] PID: %d by `%s` (use ts -k to kill).\n",
1397+
*jobid, p->pid, p->user->name);
1398+
s_send_removejob_nok(s, buff);
1399+
return 0;
1400+
}
1401+
12871402
*jobid = p->jobid;
12881403
delete_DB(p->jobid, "Jobs");
12891404
p->state = FINISHED;

jobs.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ struct Msg;
1818
enum Jobstate {
1919
QUEUED,
2020
RUNNING,
21+
ABNORMAL,
2122
PAUSE,
2223
FINISHED,
2324
SKIPPED,
@@ -74,6 +75,8 @@ struct Job {
7475
int num_allocated;
7576
int no_cpu_binding;
7677
int client_socket;
78+
/* 0=HEALTH_UNCHECKED, 1=HEALTH_NORMAL, 2=HEALTH_ABNORMAL */
79+
int health_state;
7780
#ifdef TS_CPU_BIND
7881
void *cpu_alloc; /* struct CpuAlloc * 由 cpu_bind 管理 */
7982
#endif
@@ -127,6 +130,7 @@ void s_kill_all_jobs(int s, struct User *user);
127130
void s_count_running_jobs(int s, struct User *user);
128131
void s_check_holdon(void);
129132

133+
void s_check_running_health(void);
130134
int s_check_running_pid(pid_t pid);
131135
void s_read_sqlite(void);
132136
int s_find_pid(pid_t target_pid, int deep_search);

server.c

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,15 @@ static void server_loop(int ls) {
413413
}
414414
} // nconnections
415415

416+
{ /* Periodic health check for RUNNING jobs — every ~10 seconds */
417+
static int health_ticks = 0;
418+
health_ticks++;
419+
if (health_ticks >= 10) {
420+
health_ticks = 0;
421+
s_check_running_health();
422+
}
423+
}
424+
416425
next_run_job();
417426
s_check_holdon();
418427
} // end of while (keep_loop)

0 commit comments

Comments
 (0)