blob: 201694319082185d5153e6490c94cda2417e2b4e [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Verify a Prolog failure keeps a job from finishing its configuration.
With PrologFlags=Alloc a job stays CONFIGURING until every node it is waiting
on reports its Prolog done. Once one node has reported a failure, a later
success from another node must not finish the configuration and launch the
job: slurmctld records the failure on the job for that.
slurmd sends each node's result and then its own requeue back to back, so a
success from another node only lands between the two by chance, a window too
narrow to hit from outside. This test therefore sends the reports itself, as
SlurmUser, with a small libslurm client: the failure first, then the success,
and no requeue behind them. Without the recorded failure, the success finishes
the configuration and the batch script runs.
The real Prologs block on a sentinel file the whole time, so the only reports
slurmctld sees are the ones sent here, until the test releases them.
Ticket 15001.
"""
import os
import pytest
import atf
pytestmark = pytest.mark.slow
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 11),
component="sbin/slurmctld",
reason="Ticket 15001: CONFIGURING while the Prolog runs added in 26.11",
)
# enable_stepmgr is only supported with PrologFlags=contain.
atf.require_config_parameter_excludes("SlurmctldParameters", "enable_stepmgr")
# Without DeferBatch only the batch host holds the job in CONFIGURING, so
# it is the batch host's report that finishes the configuration. Set rather
# than appended: an inherited NoHold or DeferBatch would change that.
atf.require_config_parameter("PrologFlags", "Alloc")
atf.require_nodes(2)
# The Prolog runs as root on the node, so its sentinel needs an absolute
# path outside the test's own cwd.
prolog_script = str(atf.module_tmp_path / "prolog.sh")
atf.make_bash_script(
prolog_script,
f"""while [ ! -f {atf.module_tmp_path}/proceed.$SLURM_JOB_ID ]; do
sleep 0.5
done
""",
)
atf.require_config_parameter("Prolog", prolog_script)
atf.require_slurm_running()
@pytest.fixture(scope="module")
def report_prolog():
"""Build the client that sends REQUEST_COMPLETE_PROLOG to slurmctld."""
source = os.path.splitext(__file__)[0] + ".c"
client = str(atf.module_tmp_path / "test_132_6")
atf.compile_against_libslurm(source, client, full=True, fatal=True)
def report(job_id, node_name, prolog_rc):
# slurmctld only accepts this RPC from root or SlurmUser.
output = atf.run_command_output(
f"{client} {job_id} {node_name} {prolog_rc}",
user=atf.properties["slurm-user"],
fatal=True,
)
assert output.startswith(
"RC=0 "
), f"slurmctld rejected the Prolog report: {output.strip()}"
return report
@pytest.fixture
def blocked_job():
"""Submit a two node job and release its Prologs afterwards.
--no-requeue stops the cancel at teardown from sending the job round
again with its Prologs still blocked.
"""
job_id = atf.submit_job_sbatch(
"-N2 -t2 --no-requeue -o /dev/null --wrap 'touch job_ran'", fatal=True
)
yield job_id
atf.run_command(f"touch {atf.module_tmp_path / f'proceed.{job_id}'}")
def test_success_after_failure_keeps_job_configuring(report_prolog, blocked_job):
"""A success reported after a failure must not launch the job."""
job_id = blocked_job
assert atf.wait_for_job_state(
job_id, "CONFIGURING"
), f"Job {job_id} was not held in CONFIGURING by its Prolog"
batch_host = atf.get_job_parameter(job_id, "BatchHost", fatal=True)
other_node = next(
node
for node in atf.node_range_to_list(
atf.get_job_parameter(job_id, "NodeList", fatal=True)
)
if node != batch_host
)
# The failure comes from the other node, so only the batch host's report is
# left for the job to wait on. That report is the success.
report_prolog(job_id, other_node, 1)
report_prolog(job_id, batch_host, 0)
# A window to watch, not a wait for success: a wrongful launch moves the
# job to RUNNING within the same RPC, so 10 seconds is plenty to see it.
for _ in atf.timer(timeout=10, xfail=True, quiet=True):
state = atf.get_job_parameter(job_id, "JobState", fatal=True)
if state != "CONFIGURING":
pytest.fail(
f"Job {job_id} went to {state} after a Prolog success that "
f"followed a failure"
)
assert not os.path.isfile(
"job_ran"
), "The batch script ran although a Prolog had failed"