blob: d1484955c2ed15baa5ccd21719ff086ab37f2ce6 [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Verify the job state when PrologSlurmctld runs before an Alloc Prolog.
With PrologSlurmctld the job is CONFIGURING before its nodes are told to run the
Prolog. With PrologFlags=Alloc it must then stay CONFIGURING while the node
Prolog runs, so the job only leaves CONFIGURING once both have finished. NoHold
opts out of the hold on the node Prolog, and an external job has no node Prolog
at all, so both of those must leave CONFIGURING as soon as PrologSlurmctld is
done.
Both scripts block on sentinel files rather than sleeping for a fixed time, so
each window being sampled stays open until the test releases it.
"""
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",
)
# PrologSlurmctld runs as SlurmUser on the controller and the Prolog as root
# on the node, so both key their files off the job id in the module tmp
# dir, which conftest makes reachable by every user.
ctld_script = str(atf.module_tmp_path / "prolog_slurmctld.sh")
atf.make_bash_script(
ctld_script,
f"""touch {atf.module_tmp_path}/ctld_started.$SLURM_JOB_ID
while [ ! -f {atf.module_tmp_path}/ctld_proceed.$SLURM_JOB_ID ]; do
sleep 0.5
done
touch {atf.module_tmp_path}/ctld_finished.$SLURM_JOB_ID
""",
)
prolog_script = str(atf.module_tmp_path / "prolog.sh")
atf.make_bash_script(
prolog_script,
f"""touch {atf.module_tmp_path}/started.$SLURM_JOB_ID
while [ ! -f {atf.module_tmp_path}/proceed.$SLURM_JOB_ID ]; do
sleep 0.5
done
""",
)
# enable_stepmgr is only supported with PrologFlags=contain, and these cases
# set other flags.
atf.require_config_parameter_excludes("SlurmctldParameters", "enable_stepmgr")
atf.require_config_parameter("PrologSlurmctld", ctld_script)
atf.require_config_parameter("Prolog", prolog_script)
atf.require_slurm_running()
@pytest.fixture
def release_scripts():
"""Release both scripts for every job a case submitted.
The scripts loop until released, so this keeps a failing case from leaving
them running into the next one. Only this case's jobs are released:
submitted-jobs also holds ids from earlier modules in the session, which
later jobs here can reuse.
"""
first = len(atf.properties["submitted-jobs"])
yield
for job_id in atf.properties["submitted-jobs"][first:]:
for marker in (f"ctld_proceed.{job_id}", f"proceed.{job_id}"):
atf.run_command(f"touch {atf.module_tmp_path / marker}", quiet=True)
def touch(name):
atf.run_command(f"touch {atf.module_tmp_path / name}", fatal=True)
def job_state(job_id):
return atf.get_job_parameter(job_id, "JobState", default="NOT_FOUND", quiet=True)
def wait_for_marker(name):
assert atf.wait_for_file(
str(atf.module_tmp_path / name)
), f"{name} was never created"
def assert_stays(job_id, expected, reason):
"""Assert the job stays in expected for a short window.
Used once the event that could wrongly change the state has happened, so a
regression shows up as the state changing inside the window.
"""
# A window to watch, not a wait for success, so the 45 second default would
# only slow the case down. Any wrong transition happens within seconds.
for _ in atf.timer(timeout=15, xfail=True, quiet=True):
state = job_state(job_id)
if state != expected:
pytest.fail(f"{reason}: job {job_id} went to {state}")
@pytest.mark.parametrize(
"prolog_flags,expect_configuring",
[
pytest.param("Alloc", True, id="alloc"),
pytest.param("Alloc,NoHold", False, id="alloc-nohold"),
],
)
def test_prolog_after_prolog_slurmctld(
release_scripts, tmp_path, prolog_flags, expect_configuring
):
"""Verify the node Prolog still holds the job after PrologSlurmctld.
The job is CONFIGURING while PrologSlurmctld runs and its node Prolog has
not been launched yet. Once PrologSlurmctld finishes, the node Prolog is
launched, and the job must stay CONFIGURING until that is done too, unless
NoHold is set.
"""
output_file = str(tmp_path / "job_output")
atf.set_config_parameter("PrologFlags", prolog_flags)
job_id = atf.submit_job_sbatch(
f"-N1 -t5 --wrap 'touch {output_file}; sleep 30'", fatal=True
)
wait_for_marker(f"ctld_started.{job_id}")
assert (
job_state(job_id) == "CONFIGURING"
), "Job was not CONFIGURING while PrologSlurmctld ran"
assert not os.path.isfile(
atf.module_tmp_path / f"started.{job_id}"
), "The node Prolog started before PrologSlurmctld finished"
touch(f"ctld_proceed.{job_id}")
wait_for_marker(f"ctld_finished.{job_id}")
wait_for_marker(f"started.{job_id}")
if expect_configuring:
assert_stays(
job_id,
"CONFIGURING",
"Job left CONFIGURING before its node Prolog finished",
)
assert not os.path.isfile(
output_file
), "The batch script ran before the node Prolog finished"
touch(f"proceed.{job_id}")
else:
# With NoHold the job is not held for the node Prolog. slurmctld
# finishes configuring the job before it sends the Prolog launch
# request, so the job is already RUNNING once that Prolog has started.
# Sample it at once rather than waiting for RUNNING, which the periodic
# time limit pass would also eventually produce.
state = job_state(job_id)
assert (
state == "RUNNING"
), f"Expected RUNNING while the NoHold node Prolog runs, got {state}"
touch(f"proceed.{job_id}")
assert atf.wait_for_job_state(job_id, "RUNNING"), "Job never reached RUNNING"
assert atf.wait_for_file(output_file), "Batch step never produced output"
def test_external_job_after_prolog_slurmctld(release_scripts):
"""Verify an external job leaves CONFIGURING after PrologSlurmctld.
An external job gets no node Prolog, so once PrologSlurmctld finishes there
is nothing left to hold it in CONFIGURING. It must also still end at its
time limit.
"""
atf.set_config_parameter("PrologFlags", "Alloc")
job_id = atf.submit_job_sbatch("--external -N1 -t1", fatal=True)
wait_for_marker(f"ctld_started.{job_id}")
assert (
job_state(job_id) == "CONFIGURING"
), "External job was not CONFIGURING while PrologSlurmctld ran"
touch(f"ctld_proceed.{job_id}")
wait_for_marker(f"ctld_finished.{job_id}")
assert atf.wait_for_job_state(
job_id, "RUNNING"
), "External job never left CONFIGURING after PrologSlurmctld"
assert not os.path.isfile(
atf.module_tmp_path / f"started.{job_id}"
), "A node Prolog ran for an external job"
# Longer than the default: the job has a one minute limit, and slurmctld only
# checks time limits every 30 seconds.
assert atf.wait_for_job_state(
job_id, "TIMEOUT", timeout=150
), "External job did not end at its time limit"