blob: 6684b5c4076b1bcc3c8483ad3fa49a48d8700f54 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test sacct reading jobcomp/filetxt records and the SIGUSR2 log reopen."""
import pytest
import atf
jobcomp_log = None
@pytest.fixture(scope="module", autouse=True)
def setup():
global jobcomp_log
# slurmctld needs an absolute path
jobcomp_log = atf.module_tmp_path / "jobcomp.log"
atf.require_config_parameter("JobCompType", "jobcomp/filetxt")
atf.require_config_parameter("JobCompLoc", str(jobcomp_log))
atf.require_slurm_running()
def run_job(name, command="true"):
"""Run a batch job until it is done and return its id."""
job_id = atf.submit_job_sbatch(
f"-J {name} -o /dev/null --wrap '{command}'", fatal=True
)
atf.wait_for_job_state(job_id, "DONE", fatal=True)
return job_id
def get_records(job_ids, source="-c"):
"""Return the sacct records of job_ids as {job_id: [name, state, exit code]}."""
# sacct opens the jobcomp file for writing, so run it as SlurmUser
output = atf.run_command_output(
f"sacct {source} -j {','.join(map(str, job_ids))} -n -P "
"-o jobid,jobname,state,exitcode",
user=atf.properties["slurm-user"],
)
rows = [line.split("|") for line in output.splitlines()]
return {int(row[0]): row[1:] for row in rows}
def wait_for_records(job_ids):
"""Poll sacct -c until all job_ids have a record, then return the records."""
for _ in atf.timer():
records = get_records(job_ids)
if set(job_ids) <= records.keys():
break
assert set(job_ids) <= records.keys(), f"sacct -c should list jobs {job_ids}"
return records
def test_sacct_completion():
"""sacct -c reads the job records from the jobcomp file."""
ok_id = run_job("jobcomp_ok")
failed_id = run_job("jobcomp_failed", "exit 3")
records = wait_for_records([ok_id, failed_id])
assert records[ok_id] == [
"jobcomp_ok",
"COMPLETED",
"0:0",
], f"Job {ok_id} should be COMPLETED with exit code 0:0"
assert records[failed_id] == [
"jobcomp_failed",
"FAILED",
"3:0",
], f"Job {failed_id} should be FAILED with exit code 3:0"
def test_sacct_file():
"""sacct -f reads the named file instead of JobCompLoc and implies -c."""
snapshot = atf.module_tmp_path / "snapshot.log"
old_id = run_job("jobcomp_old")
wait_for_records([old_id])
# sacct -f also opens the copy for writing
atf.run_command(
f"cp {jobcomp_log} {snapshot}", user=atf.properties["slurm-user"], fatal=True
)
new_id = run_job("jobcomp_new")
wait_for_records([old_id, new_id])
assert get_records([old_id, new_id], f"-f {snapshot}").keys() == {
old_id
}, f"sacct -f should list only job {old_id}"
@pytest.mark.parametrize("pre_create", [False, True])
def test_sigusr2_reopen(pre_create):
"""After JobCompLoc is moved, SIGUSR2 makes slurmctld log to a new file."""
rotated = atf.module_tmp_path / f"rotated_{pre_create}.log"
old_id = run_job("jobcomp_old")
wait_for_records([old_id])
atf.run_command(
f"mv {jobcomp_log} {rotated}", user=atf.properties["slurm-user"], fatal=True
)
if pre_create:
# Same as the logrotate "create 640 slurm root" option
atf.run_command(
f"install -m 640 /dev/null {jobcomp_log}",
user=atf.properties["slurm-user"],
fatal=True,
)
# This also signals slurmscriptd, which ignores SIGUSR2
pids = atf.pids_from_exe(f"{atf.properties['slurm-sbin-dir']}/slurmctld")
atf.run_command(f"kill -USR2 {' '.join(map(str, pids))}", user="root", fatal=True)
if pre_create:
# The file already exists, so wait until slurmctld has it open
fds = " ".join(f"/proc/{pid}/fd/*" for pid in pids)
for _ in atf.timer():
open_files = atf.run_command_output(f"readlink {fds}", user="root")
is_open = str(jobcomp_log) in open_files.splitlines()
if is_open:
break
assert is_open, f"SIGUSR2 should make slurmctld open {jobcomp_log}"
else:
assert atf.wait_for_file(
str(jobcomp_log)
), f"SIGUSR2 should create a new {jobcomp_log}"
new_id = run_job("jobcomp_new")
wait_for_records([new_id])
assert get_records([old_id, new_id]).keys() == {
new_id
}, f"The new file should have only job {new_id}"
assert get_records([old_id, new_id], f"-f {rotated}").keys() == {
old_id
}, f"The moved file should have only job {old_id}"