blob: 8f698fcf5c4109bf5944e103b91d819aeaa8b88c [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Retry a dependency-ready step after its allocation's reserved ports free."""
import re
import pytest
import atf
PORT_COUNT = 5
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 11),
"bin/srun",
reason="Issue 50938: reserved-port retries require real pending step IDs",
)
atf.require_version(
(26, 11),
"bin/scontrol",
reason="step dependency reporting requires a 26.11 scontrol",
)
atf.require_version(
(26, 11),
"sbin/slurmd",
reason="step dependencies require a 26.11 stepmgr",
)
atf.require_nodes(1, [("CPUs", 2)])
atf.require_config_parameter("SelectType", "select/cons_tres")
atf.require_config_parameter("SelectTypeParameters", "CR_CPU")
atf.require_config_parameter_includes("SlurmctldParameters", "enable_stepmgr")
atf.require_config_parameter_includes("PrologFlags", "Contain")
ports = re.search(
r"(?:^|,)ports=(\d+)-(\d+)(?:,|$)",
atf.get_config_parameter("MpiParams", live=False) or "",
)
if not ports or int(ports[2]) - int(ports[1]) + 1 < PORT_COUNT:
atf.require_tool("ss")
ephemeral = atf.run_command_output(
"cat /proc/sys/net/ipv4/ip_local_port_range", fatal=True
)
ephemeral_low, ephemeral_high = map(int, ephemeral.split())
for low in (ephemeral_high + 100, ephemeral_low - 100 - PORT_COUNT):
high = low + PORT_COUNT - 1
if not (1024 <= low <= high <= 65535):
continue
occupied = atf.run_command_output(
f'ss -tanH "sport >= :{low} and sport <= :{high}"', fatal=True
)
if not occupied.strip():
atf.require_config_parameter_includes(
"MpiParams", ("ports", f"{low}-{high}")
)
break
else:
pytest.fail("no available reserved-port range outside ephemeral ports")
atf.require_slurm_running()
def test_ports_busy_wake_retains_dependency_and_step_id():
"""A late ports-busy failure keeps the accepted step and its resolved edge."""
atf.make_bash_script(
"ports_busy.sh",
f"""set -e
srun --mpi=none --exact -n1 --resv-ports=0 bash -c 'touch blocker_started; until [ -f blocker_release ]; do sleep 0.2; done' &
until [ -f blocker_started ]; do sleep 0.2; done
srun --mpi=none --exact -n1 --resv-ports={PORT_COUNT} bash -c 'touch holder_started; until [ -f holder_release ]; do sleep 0.2; done' &
until [ -f holder_started ]; do sleep 0.2; done
srun --mpi=none --exact -n1 --async --parsable --resv-ports=1 --step-dependency=afterok:$SLURM_JOB_ID.0 --output=target.out --error=target.err bash -c 'touch target_started; until [ -f target_release ]; do sleep 0.2; done; echo TARGET_DONE' > target_id
until scontrol -o show step "$SLURM_JOB_ID.2" | grep -q 'Reason=Dependency '; do sleep 0.2; done
touch blocker_release
while scontrol -o show step "$SLURM_JOB_ID.0" 2>/dev/null | grep -q 'StepId='; do sleep 0.2; done
# One CPU is free and the dependency resolved, but holder owns every job port.
until scontrol -o show step "$SLURM_JOB_ID.2" | grep -q 'Reason=Resources '; do sleep 0.2; done
scontrol -o show step "$SLURM_JOB_ID.2" > ports_busy_state
test ! -f target_started
touch holder_release
until [ -f target_started ]; do sleep 0.2; done
until scontrol -o show step "$SLURM_JOB_ID.2" | grep -q 'State=RUNNING'; do sleep 0.2; done
scontrol -o show step "$SLURM_JOB_ID.2" > ports_running_state
touch target_release
wait
swait --timeout=60
echo SCRIPT_DONE
""",
)
# The allocation owns exactly PORT_COUNT ports, independently of a larger
# site-wide pool; holder exhausts only this job's stepmgr port table.
job_id = atf.submit_job_sbatch(
f"-N1 -n2 -t3 --resv-ports={PORT_COUNT} "
"--output=ports_busy.out --error=ports_busy.out ports_busy.sh",
fatal=True,
)
atf.wait_for_job_state(job_id, "COMPLETED", fatal=True)
atf.assert_file_contents("ports_busy.out", "SCRIPT_DONE", contains=True)
atf.assert_file_contents("target_id", f"{job_id}.2")
atf.assert_file_contents("ports_busy_state", f"StepId={job_id}.2 ", contains=True)
atf.assert_file_contents("ports_busy_state", "State=PENDING", contains=True)
atf.assert_file_contents("ports_busy_state", "Reason=Resources", contains=True)
atf.assert_file_contents(
"ports_running_state", f"StepId={job_id}.2 ", contains=True
)
atf.assert_file_contents("ports_running_state", "State=RUNNING", contains=True)
atf.assert_file_contents(
"ports_running_state", f"StepDependency=afterok:{job_id}.0", contains=True
)
atf.assert_file_contents("target.out", "TARGET_DONE", contains=True)