Skip to content

Commit 73b4063

Browse files
authored
Fail a SLURM job the scheduler marked FAILED, whatever its exit code (#1879)
1 parent 3e95b42 commit 73b4063

3 files changed

Lines changed: 75 additions & 6 deletions

File tree

.github/scripts/monitor_slurm_job.sh

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ while true; do
208208
fi
209209

210210
if is_terminal_state "$state"; then
211+
final_state="$state"
211212
echo "[$(date +%H:%M:%S)] Job $job_id reached terminal state: $state"
212213
break
213214
fi
@@ -308,16 +309,26 @@ case "$exit_code" in
308309
;;
309310
esac
310311

311-
# Check if job succeeded
312-
if [ "$exit_code" != "0:0" ]; then
313-
echo "ERROR: Job $job_id failed with exit code $exit_code"
312+
# Check if job succeeded.
313+
#
314+
# Both the recorded state and the exit code have to agree. SLURM intermittently
315+
# reports State=FAILED alongside ExitCode=0:0 -- on Phoenix the same job that
316+
# printed "reached terminal state: FAILED" here then reported "completed
317+
# successfully" and went green with 23 failing tests still in its log. Five of
318+
# nine "successful" gpu-acc runs on master were hiding failures that way, which
319+
# made a green self-hosted job worthless as evidence.
320+
#
321+
# run_monitored_slurm_job.sh already re-checks the state via sacct, but only
322+
# when this script exits non-zero, so nothing verified it on the success path.
323+
if [ "$exit_code" != "0:0" ] || { [ -n "${final_state:-}" ] && [ "$final_state" != "COMPLETED" ]; }; then
324+
echo "ERROR: Job $job_id failed (state=${final_state:-unknown}, exit code $exit_code)"
314325
# A GPU memory fault explains itself in a block the test harness prints; lift
315326
# it onto the summary page so the faulting kernel and source line are visible
316327
# without opening the log at all.
317328
if grep -q 'GPU fault summary' "$output_file" 2>/dev/null; then
318329
ci_summary "### GPU memory fault\n\n\`\`\`\n$(grep -A6 'GPU fault summary' "$output_file" | head -8 | sed 's/`/'"'"'/g')\n\`\`\`\n"
319330
else
320-
ci_summary "### Job \`$job_id\` failed (exit $exit_code)\n\n\`\`\`\n$(tail -n 15 "$output_file" | sed 's/`/'"'"'/g')\n\`\`\`\n"
331+
ci_summary "### Job \`$job_id\` failed (state ${final_state:-unknown}, exit $exit_code)\n\n\`\`\`\n$(tail -n 15 "$output_file" | sed 's/`/'"'"'/g')\n\`\`\`\n"
321332
fi
322333
exit 1
323334
fi

toolchain/mfc/test_monitor_ci_summary.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,20 @@ def slurm(tmp_path):
3131
binz = tmp_path / "bin"
3232
binz.mkdir()
3333

34-
def configure(exit_code, output):
34+
# The reported state has to track the exit code: the monitor now requires both
35+
# to say the job succeeded, so a stub that always says FAILED would describe a
36+
# job SLURM never produces for a clean run.
37+
def configure(exit_code, output, state=None):
38+
if state is None:
39+
state = "COMPLETED" if exit_code == "0:0" else "FAILED"
40+
3541
def exe(name, body):
3642
path = binz / name
3743
path.write_text(body)
3844
path.chmod(path.stat().st_mode | stat.S_IEXEC)
3945

4046
exe("squeue", "#!/bin/bash\nexit 0\n")
41-
exe("sacct", f'#!/bin/bash\nfor a in "$@"; do [ "$a" = "--format=ExitCode" ] && {{ echo "{exit_code}"; exit 0; }}; done\necho FAILED\n')
47+
exe("sacct", f'#!/bin/bash\nfor a in "$@"; do [ "$a" = "--format=ExitCode" ] && {{ echo "{exit_code}"; exit 0; }}; done\necho "{state}"\n')
4248
exe("scontrol", f'#!/bin/bash\necho "ExitCode={exit_code}"\n')
4349
exe("scancel", "#!/bin/bash\nexit 0\n")
4450

toolchain/mfc/test_monitor_exit_codes.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,3 +91,55 @@ def test_the_runner_relays_the_infrastructure_exit_code(tmp_path, monitor_exit):
9191
timeout=180,
9292
)
9393
assert result.returncode == monitor_exit
94+
95+
96+
def test_monitor_fails_a_job_slurm_marked_failed_despite_a_zero_exit_code(slurm):
97+
"""A terminal state of FAILED is a failure even when ExitCode reads 0:0.
98+
99+
SLURM reports that combination intermittently on Phoenix. Deciding purely on
100+
the exit code turned those jobs green with the test failures still in the
101+
log -- 5 of 9 "successful" gpu-acc runs on master were hiding 23 failing
102+
tests this way.
103+
"""
104+
tmp_path, binz, configure = slurm
105+
out = configure("0:0", state="FAILED")
106+
assert run_script(tmp_path, binz, "monitor_slurm_job.sh", "1234", str(out)).returncode == 1
107+
108+
109+
def test_monitor_still_passes_a_genuinely_completed_job(slurm):
110+
"""The guard above must not turn healthy jobs red."""
111+
tmp_path, binz, configure = slurm
112+
out = configure("0:0", state="COMPLETED")
113+
assert run_script(tmp_path, binz, "monitor_slurm_job.sh", "1234", str(out)).returncode == 0
114+
115+
116+
# Every terminal state SLURM can report for a job that did not complete. The pairing
117+
# with 0:0 is the one the scheduler actually produces intermittently, and the one that
118+
# used to be read as success.
119+
NOT_COMPLETED = [
120+
"FAILED",
121+
"CANCELLED",
122+
"CANCELLED+",
123+
"TIMEOUT",
124+
"OUT_OF_MEMORY",
125+
"NODE_FAIL",
126+
"BOOT_FAIL",
127+
"DEADLINE",
128+
"REVOKED",
129+
]
130+
131+
132+
@pytest.mark.parametrize("state", NOT_COMPLETED)
133+
def test_a_zero_exit_code_does_not_rescue_a_job_that_did_not_complete(slurm, state):
134+
tmp_path, binz, configure = slurm
135+
out = configure("0:0", state=state)
136+
assert run_script(tmp_path, binz, "monitor_slurm_job.sh", "1234", str(out)).returncode == 1
137+
138+
139+
@pytest.mark.parametrize("state", NOT_COMPLETED)
140+
def test_an_infrastructure_fault_still_outranks_the_state(slurm, state):
141+
"""77 has to survive: the submit wrapper uses it to exclude the node and resubmit,
142+
and flattening it to a generic failure would strand the job on a bad node."""
143+
tmp_path, binz, configure = slurm
144+
out = configure("77:0", state=state)
145+
assert run_script(tmp_path, binz, "monitor_slurm_job.sh", "1234", str(out)).returncode == 77

0 commit comments

Comments
 (0)